refactor(coderd): restore agents-access cleanup migration (#28587)

## Summary

- restore the `agents-access` data cleanup migration as `000587`
(renumbered from `000585`; main advanced past that number)
- revert the temporary RBAC tolerance for retired role names now that
the migration strips stale grants
- make the migration test discover its predecessor dynamically, since
this branch does not carry main's `000585`/`000586`

## Stack context

Stacked on #28186, which retires the `agents-access` role but
intentionally ships without a migration so it can be backported across
diverged migration numbers. This layer completes the retirement on main:
it cleans up stale role grants in `users.rbac_roles`,
`organization_members.roles`, and
`organizations.default_org_member_roles`, then reverts the interim
leniency (role expansion dropping legacy names, assignment validation
ignoring them) that existed only to keep the migration-less base safe.
`rbac.ReservedRoleName` still reserves the retired name so custom roles
cannot shadow it on rollback. Do not backport this PR alongside the base
PR.

## Rollback note

The down migration restores effective access broadly because it cannot
reconstruct which users previously held the removed role.

## Validation

- `go build ./coderd/... ./enterprise/...`
- full `coderd/database/migrations`, `coderd/rbac/...`, and
`coderd/database/dbauthz` suites
- targeted members tests in `coderd`
- repository pre-commit checks (`lint/go`, `lint/ts`, etc.)

> Mux created this pull request on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-08-26 15:29:26 +02:00
committed by GitHub
parent 31146642ba
commit 45f2eb48cc
8 changed files with 107 additions and 173 deletions
+5 -14
View File
@@ -847,12 +847,6 @@ func SlimRolesFromNames(names []string) []codersdk.SlimRole {
convertedRoles := make([]codersdk.SlimRole, 0, len(names))
for _, name := range names {
// Stored role arrays may retain retired built-in role names until
// a cleanup migration lands. Hide them so consumers do not display
// or resubmit them.
if rbac.IsRetiredRoleName(name) {
continue
}
convertedRoles = append(convertedRoles, SlimRoleFromName(name))
}
@@ -925,14 +919,11 @@ func Organization(organization database.Organization) codersdk.Organization {
DisplayName: organization.DisplayName,
Icon: organization.Icon,
},
Description: organization.Description,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
IsDefault: organization.IsDefault,
// Stored default role lists may retain retired built-in role names
// until a cleanup migration lands. Hide them so settings forms do
// not display or resubmit them.
DefaultOrgMemberRoles: slices.DeleteFunc(slices.Clone(organization.DefaultOrgMemberRoles), rbac.IsRetiredRoleName),
Description: organization.Description,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
IsDefault: organization.IsDefault,
DefaultOrgMemberRoles: organization.DefaultOrgMemberRoles,
}
}
@@ -0,0 +1,13 @@
-- Older binaries need agents-access unless another role grants chat access.
-- Exact prior grants cannot be reconstructed, so restore the role to all
-- current non-service-account memberships.
UPDATE organization_members om
SET roles = array_append(om.roles, 'agents-access')
FROM users u
WHERE u.id = om.user_id
AND NOT u.is_service_account
AND NOT ('agents-access' = ANY(om.roles));
-- Defaults are not restored: older binaries union default_org_member_roles
-- into service-account memberships too. Pre-upgrade explicit service-account
-- grants stay lost; an admin can re-grant them.
@@ -0,0 +1,11 @@
UPDATE users
SET rbac_roles = array_remove(rbac_roles, 'agents-access')
WHERE 'agents-access' = ANY(rbac_roles);
UPDATE organization_members
SET roles = array_remove(roles, 'agents-access')
WHERE 'agents-access' = ANY(roles);
UPDATE organizations
SET default_org_member_roles = array_remove(default_org_member_roles, 'agents-access')
WHERE 'agents-access' = ANY(default_org_member_roles);
@@ -1191,6 +1191,74 @@ func TestMigration000475AgentsAccessOrgRole(t *testing.T) {
)
}
func TestMigration000587RemoveAgentsAccessRole(t *testing.T) {
t.Parallel()
const migrationVersion = 587
// The immediately preceding migration numbers may not exist in this
// tree (the target is numbered past migrations that landed on main
// separately), so step to the highest version below the target rather
// than assuming migrationVersion-1 exists.
entries, err := os.ReadDir(".")
require.NoError(t, err)
prevVersion := uint(0)
for _, entry := range entries {
var version uint
if _, err := fmt.Sscanf(entry.Name(), "%d_", &version); err != nil {
continue
}
if version > prevVersion && version < migrationVersion {
prevVersion = version
}
}
require.NotZero(t, prevVersion)
sqlDB := testSQLDB(t)
next, err := migrations.Stepper(sqlDB)
require.NoError(t, err)
for {
version, more, err := next()
require.NoError(t, err)
if !more {
t.Fatalf("migration %d not found", migrationVersion)
}
if version == prevVersion {
break
}
}
db := database.New(sqlDB)
user := dbgen.User(t, db, database.User{
RBACRoles: []string{"auditor", "agents-access"},
})
org := dbgen.Organization(t, db, database.Organization{
DefaultOrgMemberRoles: []string{"organization-workspace-access", "agents-access"},
})
dbgen.OrganizationMember(t, db, database.OrganizationMember{
OrganizationID: org.ID,
UserID: user.ID,
Roles: []string{"organization-auditor", "agents-access"},
})
version, _, err := next()
require.NoError(t, err)
require.EqualValues(t, migrationVersion, version)
ctx := testutil.Context(t, testutil.WaitLong)
var siteRoles, orgRoles, defaultRoles pq.StringArray
err = sqlDB.QueryRowContext(ctx, "SELECT rbac_roles FROM users WHERE id = $1", user.ID).Scan(&siteRoles)
require.NoError(t, err)
err = sqlDB.QueryRowContext(ctx, "SELECT roles FROM organization_members WHERE organization_id = $1 AND user_id = $2", org.ID, user.ID).Scan(&orgRoles)
require.NoError(t, err)
err = sqlDB.QueryRowContext(ctx, "SELECT default_org_member_roles FROM organizations WHERE id = $1", org.ID).Scan(&defaultRoles)
require.NoError(t, err)
require.Equal(t, []string{"auditor"}, []string(siteRoles))
require.Equal(t, []string{"organization-auditor"}, []string(orgRoles))
require.Equal(t, []string{"organization-workspace-access"}, []string(defaultRoles))
}
func TestMigration000504AIProvidersBackfill(t *testing.T) {
t.Parallel()