diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 4713bbfeeee49..9e0b19986c8b9 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -847,6 +847,12 @@ 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)) } @@ -919,11 +925,14 @@ 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, - DefaultOrgMemberRoles: organization.DefaultOrgMemberRoles, + 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), } } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 7dd08483218f1..3bb40f84678c9 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1413,6 +1413,15 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID uuid.UUID, added, re grantedRoles := make([]rbac.RoleIdentifier, 0, len(added)+len(removed)) grantedRoles = append(grantedRoles, added...) grantedRoles = append(grantedRoles, removed...) + // Retired role names may linger in stored role arrays and org default + // role lists until a data cleanup migration lands. They expand to no + // permissions and cannot be re-created as custom roles, so validating + // them is unnecessary: the added set only contains them via stored + // data (implied org defaults), never via explicit grants, which the + // role-update paths reject before reaching this filter. + grantedRoles = slices.DeleteFunc(grantedRoles, func(r rbac.RoleIdentifier) bool { + return rbac.IsRetiredRoleName(r.Name) + }) customRoles := make([]rbac.RoleIdentifier, 0) // Validate that the roles being assigned are valid. for _, r := range grantedRoles { @@ -7866,6 +7875,16 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb if err != nil { return database.OrganizationMember{}, err } + // Explicitly granting a retired role is rejected. Retired-name tolerance + // only covers stored grants and implied org defaults that linger until + // the cleanup migration lands; without this check the request would + // skip validation and persist a hidden grant that a binary rollback + // resolves again. + for _, role := range scopedGranted { + if rbac.IsRetiredRoleName(role.Name) { + return database.OrganizationMember{}, xerrors.Errorf("role %q is retired and cannot be assigned", role.Name) + } + } // The org's default_org_member_roles are implied at request time by // GetAuthorizationUserRoles. Include them in the implied set so @@ -7942,6 +7961,15 @@ func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrg scopedOrgRoleIdentifiers(existing.DefaultOrgMemberRoles, arg.ID), scopedOrgRoleIdentifiers(arg.DefaultOrgMemberRoles, arg.ID), ) + // Newly added defaults must not include retired names, which + // canAssignRoles tolerates only so stale stored defaults keep + // working until the cleanup migration lands. Removals stay + // tolerated so those stale defaults can be cleaned up. + for _, role := range added { + if rbac.IsRetiredRoleName(role.Name) { + return database.Organization{}, xerrors.Errorf("role %q is retired and cannot be a default role", role.Name) + } + } if err := q.canAssignRoles(ctx, arg.ID, added, removed); err != nil { return database.Organization{}, err } @@ -8492,6 +8520,15 @@ func (q *querier) UpdateUserRoles(ctx context.Context, arg database.UpdateUserRo return database.User{}, err } + // Explicitly granting a retired role is rejected. Retired-name tolerance + // only covers stored grants that linger until the cleanup migration + // lands. + for _, roleName := range arg.GrantedRoles { + if rbac.IsRetiredRoleName(roleName) { + return database.User{}, xerrors.Errorf("role %q is retired and cannot be assigned", roleName) + } + } + // The member role is always implied. impliedTypes := append(q.convertToDeploymentRoles(arg.GrantedRoles), rbac.RoleMember()) // If the changeset is nothing, less rbac checks need to be done. diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index b2f46bb0be4ce..22fdc81717a32 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1465,8 +1465,8 @@ func TestGetAuthorizedChats(t *testing.T) { org := dbgen.Organization(t, db, database.Organization{}) dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID}) - dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: member.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}}) - dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: secondMember.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: member.ID, OrganizationID: org.ID}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: secondMember.ID, OrganizationID: org.ID}) // Create FK dependencies: a chat provider and model config. _ = dbgen.ChatProvider(t, db, database.ChatProvider{ @@ -1653,7 +1653,7 @@ func TestGetAuthorizedChats(t *testing.T) { // Use a dedicated user for pagination to avoid interference // with the other parallel subtests. paginationUser := dbgen.User(t, db, database.User{}) - dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: paginationUser.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: paginationUser.ID, OrganizationID: org.ID}) for i := range 7 { dbgen.Chat(t, db, database.Chat{ OrganizationID: org.ID, @@ -1727,12 +1727,10 @@ func TestGetAuthorizedChatsACLSharing(t *testing.T) { dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: owner.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: recipient.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"}) @@ -1845,12 +1843,10 @@ func TestGetAuthorizedChatsACLSharingGroupACL(t *testing.T) { dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: owner.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: recipient.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID}) dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: recipient.ID, GroupID: group.ID}) @@ -1949,12 +1945,10 @@ func TestGetAuthorizedChatsByChatFileIDACLSharing(t *testing.T) { dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: owner.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: recipient.ID, OrganizationID: org.ID, - Roles: []string{rbac.RoleAgentsAccess()}, }) dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"}) diff --git a/coderd/exp_chats_acl_test.go b/coderd/exp_chats_acl_test.go index 837dd590e045d..a4c6a79098395 100644 --- a/coderd/exp_chats_acl_test.go +++ b/coderd/exp_chats_acl_test.go @@ -463,7 +463,7 @@ func TestListChatsSharedScope(t *testing.T) { client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) viewerClientExp := codersdk.NewExperimentalClient(viewerClient) sharedChat := dbgen.Chat(t, db, database.Chat{ OrganizationID: firstUser.OrganizationID, @@ -562,7 +562,7 @@ func TestChatSharingDisabled(t *testing.T) { }) firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) viewerClientExp := codersdk.NewExperimentalClient(viewerClient) chat := dbgen.Chat(t, store, database.Chat{ diff --git a/coderd/exp_chats_model_config_list_test.go b/coderd/exp_chats_model_config_list_test.go index d746f9b0bdfc8..5554f2eca9fc7 100644 --- a/coderd/exp_chats_model_config_list_test.go +++ b/coderd/exp_chats_model_config_list_test.go @@ -189,13 +189,12 @@ func TestChatModelConfigListReadContracts(t *testing.T) { seesDenied bool }{ { - name: "AgentsAccess", + name: "Member", client: func(t *testing.T, _ context.Context) *codersdk.ExperimentalClient { rawClient, _ := coderdtest.CreateAnotherUser( t, client.Client, defaultOrg.ID, - rbac.ScopedRoleAgentsAccess(defaultOrg.ID), ) return codersdk.NewExperimentalClient(rawClient) }, diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index a906efd7f32ab..0bc424ec338d4 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -732,7 +732,7 @@ func TestPostChats(t *testing.T) { Name: database.EveryoneGroup, OrganizationID: org.ID, }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID, rbac.ScopedRoleAgentsAccess(org.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) contextLimit := int64(4096) @@ -763,7 +763,7 @@ func TestPostChats(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) _ = createChatModel(t, client) org := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID, rbac.ScopedRoleAgentsAccess(org.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -794,7 +794,7 @@ func TestPostChats(t *testing.T) { }, func(params *database.InsertChatModelConfigParams) { params.Enabled = false }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID, rbac.ScopedRoleAgentsAccess(org.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -817,7 +817,7 @@ func TestPostChats(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) defaultConfig := createChatModel(t, client) otherOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, otherOrg.ID, rbac.ScopedRoleAgentsAccess(otherOrg.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, otherOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -841,7 +841,7 @@ func TestPostChats(t *testing.T) { defaultConfig := createChatModel(t, client) chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) thirdOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) thirdConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{ AIProviderID: uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true}, @@ -874,9 +874,7 @@ func TestPostChats(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - // Use a member with agents-access instead of the owner to - // verify least-privilege access. - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -947,7 +945,7 @@ func TestPostChats(t *testing.T) { OrganizationID: secondOrg.ID, IsDefault: true, }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID, rbac.ScopedRoleAgentsAccess(secondOrg.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -983,7 +981,7 @@ func TestPostChats(t *testing.T) { OrganizationID: secondOrg.ID, Enabled: true, }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID, rbac.ScopedRoleAgentsAccess(secondOrg.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1032,7 +1030,7 @@ func TestPostChats(t *testing.T) { }) require.NoError(t, err) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err = memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1087,7 +1085,7 @@ func TestPostChats(t *testing.T) { Enabled: true, }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1168,7 +1166,7 @@ func TestPostChats(t *testing.T) { OrganizationID: secondOrg.ID, IsDefault: true, }) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID, rbac.ScopedRoleAgentsAccess(secondOrg.ID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1186,7 +1184,7 @@ func TestPostChats(t *testing.T) { require.Equal(t, "Invalid IDs: "+thirdOrgConfig.ID.String(), sdkErr.Detail) }) - t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Run("MemberCanCreateChat", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -1194,20 +1192,20 @@ func TestPostChats(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) _ = createChatModel(t, client) - // Member without agents-access should be denied. - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) - _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ + chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ OrganizationID: firstUser.OrganizationID, Content: []codersdk.ChatInputPart{ { Type: codersdk.ChatInputPartTypeText, - Text: "this should fail", + Text: "hello from an organization member", }, }, }) - requireSDKError(t, err, http.StatusForbidden) + require.NoError(t, err) + require.Equal(t, member.ID, chat.OwnerID) }) t.Run("WithReasoningEffort", func(t *testing.T) { @@ -1392,7 +1390,6 @@ func TestPostChats(t *testing.T) { t, client.Client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), ) memberClient := codersdk.NewExperimentalClient(memberClientRaw) @@ -1425,7 +1422,6 @@ func TestPostChats(t *testing.T) { t, client.Client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), ) memberClient := codersdk.NewExperimentalClient(memberClientRaw) @@ -1533,7 +1529,7 @@ func TestPostChats(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) adminClient, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, adminClient.Client) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ @@ -1570,7 +1566,6 @@ func TestPostChats(t *testing.T) { adminClient.Client, firstUser.OrganizationID, rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID), - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), ) orgAdminClient := codersdk.NewExperimentalClient(orgAdminClientRaw) @@ -1739,7 +1734,7 @@ func TestPostChats(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) _ = createChatModel(t, client) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1761,7 +1756,7 @@ func TestPostChats(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) _ = createChatModel(t, client) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) // Create a second organization via the database since the @@ -1842,7 +1837,7 @@ func TestChats_ForceOnMCPServerEnforced(t *testing.T) { // A regular member tampers with the request by clearing // mcp_server_ids (Cure53 CDM-02-010 reproduction). - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ @@ -1881,7 +1876,7 @@ func TestPostChats_ClientType(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) _ = createChatModel(t, client) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) newChat := func(t *testing.T, clientType codersdk.ChatClientType) codersdk.Chat { @@ -1986,7 +1981,7 @@ func TestListChats(t *testing.T) { }) require.NoError(t, err) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) memberDBChat := dbgen.Chat(t, db, database.Chat{ OrganizationID: firstUser.OrganizationID, @@ -2060,7 +2055,7 @@ func TestListChats(t *testing.T) { modelConfig := createChatModel(t, client) ownerClientRaw, owner := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleOwner()) ownerClient := codersdk.NewExperimentalClient(ownerClientRaw) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) ownedChat := dbgen.Chat(t, db, database.Chat{ @@ -2136,18 +2131,13 @@ func TestListChats(t *testing.T) { require.NotContains(t, memberChatIDs, unsharedReadableChat.ID) }) - t.Run("OrgMemberWithoutAgentsAccessCannotAccessOwnChats", func(t *testing.T) { + t.Run("OrgMemberCanAccessOwnChats", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - // Create a member without agents-access and insert a chat - // owned by them via system context. Without agents-access, - // the member has no ResourceChat permissions at all, so - // listing returns 0 chats (SQL auth filter) and getting - // a specific chat returns 404 (dbauthz wraps as not found). memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ @@ -2157,18 +2147,15 @@ func TestListChats(t *testing.T) { Title: "member chat", }) - // Listing chats returns empty because the SQL auth - // filter excludes chats the member cannot read. chats, err := memberClient.ListChats(ctx, nil) require.NoError(t, err) - require.Len(t, chats, 0) + require.Len(t, chats, 1) + require.Equal(t, chat.ID, chats[0].ID) - // Getting a specific chat returns 404 because dbauthz - // wraps authorization failures as not-found. err = memberClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{ Title: ptr.Ref("new title"), }) - requireSDKError(t, err, http.StatusNotFound) + require.NoError(t, err) }) t.Run("Unauthenticated", func(t *testing.T) { @@ -2728,7 +2715,7 @@ func TestListChatModels(t *testing.T) { AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, OrganizationID: firstUser.OrganizationID, Model: "private-" + uuid.NewString(), Enabled: true, GroupACL: database.ChatACL{}, }) - memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) response, err := codersdk.NewExperimentalClient(memberRaw).ChatModels(ctx, firstUser.OrganizationID) require.NoError(t, err) require.False(t, containsModel(response, privateConfig.ID)) @@ -2748,7 +2735,7 @@ func TestListChatModels(t *testing.T) { require.NoError(t, err) third, err := client.CreateChatModel(ctx, thirdOrg.ID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "third-" + uuid.NewString(), ContextLimit: &contextLimit}) require.NoError(t, err) - memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) response, err := codersdk.NewExperimentalClient(memberRaw).ChatModels(ctx, chatOrg.ID) require.NoError(t, err) require.True(t, containsModel(response, local.ID)) @@ -2854,7 +2841,7 @@ func TestListChatModels(t *testing.T) { client := newChatClient(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) provider := createAIProviderForTest(t, client, "anthropic", "") - otherRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) other := codersdk.NewExperimentalClient(otherRaw) _, err := client.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "owner-key"}) require.NoError(t, err) @@ -5807,7 +5794,7 @@ func TestUpdateChatModel(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) adminClient := newChatClient(t) firstUser := coderdtest.CreateFirstUser(t, adminClient.Client) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) modelConfig := createChatModel(t, adminClient) @@ -6497,7 +6484,7 @@ func TestGetChat(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChat(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -8493,7 +8480,7 @@ func TestPostChatMessages(t *testing.T) { IsDefault: true, OrganizationID: chatOrg.ID, }) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: chatOrg.ID, @@ -8523,7 +8510,7 @@ func TestPostChatMessages(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) foreignConfig := createChatModel(t, client) chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: chatOrg.ID, @@ -8578,7 +8565,7 @@ func TestPostChatMessages(t *testing.T) { require.Equal(t, "Ask an organization administrator to configure and enable a chat model.", sdkErr.Detail) }) - t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Run("MemberCanCreateChatMessage", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -8586,11 +8573,6 @@ func TestPostChatMessages(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - // Create a member without agents-access and insert a - // chat owned by them via system context. Without - // agents-access the member has no ResourceChat - // permissions, so the ChatParam middleware returns 404 - // before the handler can check agents-access. memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ @@ -8604,11 +8586,11 @@ func TestPostChatMessages(t *testing.T) { Content: []codersdk.ChatInputPart{ { Type: codersdk.ChatInputPartTypeText, - Text: "this should fail", + Text: "hello from member", }, }, }) - requireSDKError(t, err, http.StatusNotFound) + require.NoError(t, err) }) t.Run("EmptyText", func(t *testing.T) { @@ -10222,7 +10204,7 @@ func TestPatchChatMessage(t *testing.T) { IsDefault: true, OrganizationID: chatOrg.ID, }) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: chatOrg.ID, @@ -10260,7 +10242,7 @@ func TestPatchChatMessage(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) foreignConfig := createChatModel(t, client) chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: chatOrg.ID, @@ -11812,7 +11794,7 @@ func TestGetChatDiffStatus(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChat(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -11921,7 +11903,7 @@ func TestGetChatDiffContents(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChatDiffContents(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -12082,7 +12064,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client.Client) foreignConfig := createChatModel(t, client) chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false}) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID, rbac.ScopedRoleAgentsAccess(chatOrg.ID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: chatOrg.ID, @@ -12150,7 +12132,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { require.Contains(t, sdkErr.Detail, "invalid syntax") }) - t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Run("MemberCanPromoteOwnQueuedMessage", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -12158,21 +12140,18 @@ func TestPromoteChatQueuedMessage(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - // Create a member without agents-access. Without - // agents-access the member has no ResourceChat - // permissions, so the ChatParam middleware returns 404 - // before the handler can check agents-access. memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ OrganizationID: firstUser.OrganizationID, OwnerID: member.ID, LastModelConfigID: modelConfig.ID, - Title: "promote queued no agents access", + Title: "promote queued as member", + Status: database.ChatStatusError, }) queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{ - codersdk.ChatMessageText("queued message no agents access"), + codersdk.ChatMessageText("queued message promoted by member"), }) require.NoError(t, err) queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) @@ -12185,7 +12164,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { ) require.NoError(t, err) defer promoteRes.Body.Close() - require.Equal(t, http.StatusNotFound, promoteRes.StatusCode) + require.Equal(t, http.StatusAccepted, promoteRes.StatusCode) }) t.Run("ArchivedChat", func(t *testing.T) { @@ -12680,20 +12659,20 @@ This arrived as octet-stream. requireSDKError(t, err, http.StatusUnauthorized) }) - t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Run("MemberCanUploadChatFile", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - // Member without agents-access should be denied. memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) data := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, make([]byte, 64)...) - _, err := memberClient.UploadChatFile(ctx, firstUser.OrganizationID, "image/png", "test.png", bytes.NewReader(data)) - requireSDKError(t, err, http.StatusForbidden) + uploaded, err := memberClient.UploadChatFile(ctx, firstUser.OrganizationID, "image/png", "test.png", bytes.NewReader(data)) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, uploaded.ID) }) } @@ -12877,7 +12856,7 @@ func TestGetChatFile(t *testing.T) { uploaded, err := client.UploadChatFile(ctx, firstUser.OrganizationID, "image/png", "test.png", bytes.NewReader(data)) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, _, err = otherClient.GetChatFile(ctx, uploaded.ID) requireSDKError(t, err, http.StatusNotFound) @@ -13023,7 +13002,7 @@ func TestChatFileDownloadURL(t *testing.T) { client, _ := newClient(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) uploaded, _ := uploadPNG(t, ctx, client, firstUser.OrganizationID, "owner.png") - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err := otherClient.ChatFileDownloadURL(ctx, uploaded.ID) @@ -13046,7 +13025,7 @@ func TestChatFileDownloadURL(t *testing.T) { }) require.NoError(t, err) - memberRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberRaw) err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{ UserRoles: map[string]codersdk.ChatRole{member.ID.String(): codersdk.ChatRoleRead}, @@ -13074,7 +13053,7 @@ func TestChatFileDownloadURL(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client, _ := newClient(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - memberRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberRaw) uploaded, _ := uploadPNG(t, ctx, memberClient, firstUser.OrganizationID, "suspended.png") @@ -13449,9 +13428,7 @@ func TestGetChatCost(t *testing.T) { client, db := newChatClientWithDatabase(t) firstUser := coderdtest.CreateFirstUser(t, client.Client) - // agents-access is what grants ResourceChat; plain members cannot - // create or read chats at all, so they never reach this endpoint. - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) modelConfig := createChatModel(t, client) everyoneGroup := uuid.NullUUID{UUID: firstUser.OrganizationID, Valid: true} @@ -13617,21 +13594,13 @@ func TestWatchChatGitAuthz(t *testing.T) { }) require.NoError(t, err) - // Demote adminClient via the second owner. template-admin grants - // workspace:read (site) but not workspace:ssh or - // workspace:application_connect; agents-access preserves - // chat:create|read|update on chats the user owns, so the - // demoted user still passes ExtractChatParam for their own chat. + // Demote adminClient to template-admin. Membership still allows reading this + // chat, so ExtractChatParam passes before workspace authorization fails. _, err = secondAdminClient.UpdateUserRoles(ctx, firstUser.UserID.String(), codersdk.UpdateRoles{ Roles: []string{rbac.RoleTemplateAdmin().String()}, }) require.NoError(t, err) - _, err = secondAdminClient.UpdateOrganizationMemberRoles(ctx, firstUser.OrganizationID, firstUser.UserID.String(), codersdk.UpdateRoles{ - Roles: []string{rbac.RoleAgentsAccess()}, - }) - require.NoError(t, err) - res, err := adminClient.Request( ctx, http.MethodGet, @@ -15698,7 +15667,6 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) { t, adminClient.Client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), ) memberClient := codersdk.NewExperimentalClient(memberClientRaw) @@ -15825,7 +15793,6 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) { t, adminClient.Client, org.ID, - rbac.ScopedRoleAgentsAccess(org.ID), ) otherClient := codersdk.NewExperimentalClient(otherClientRaw) @@ -16225,7 +16192,7 @@ func TestChatDebugRuns(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) chat := dbgen.Chat(t, db, database.Chat{ @@ -16337,7 +16304,7 @@ func TestChatDebugRuns(t *testing.T) { seedChatDebugRun(ctx, t, db, chat.ID, time.Now().UTC()) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID)) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err := otherClient.GetChatDebugRuns(ctx, chat.ID) @@ -17635,7 +17602,6 @@ func TestSubmitToolResults(t *testing.T) { // to user A's chat. otherClientRaw, _ := coderdtest.CreateAnotherUser( t, client.Client, user.OrganizationID, - rbac.ScopedRoleAgentsAccess(user.OrganizationID), ) otherClient := codersdk.NewExperimentalClient(otherClientRaw) @@ -17647,7 +17613,7 @@ func TestSubmitToolResults(t *testing.T) { requireSDKError(t, err, http.StatusNotFound) }) - t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Run("MemberCanSubmitToolResults", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -17655,24 +17621,20 @@ func TestSubmitToolResults(t *testing.T) { firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModel(t, client) - // Create a member without agents-access. Without - // agents-access the member has no ResourceChat - // permissions, so the ChatParam middleware returns 404 - // before the handler can check agents-access. memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) memberClient := codersdk.NewExperimentalClient(memberClientRaw) const toolName = "my_dynamic_tool" - toolCallIDs := []string{"call_noaccess"} + toolCallIDs := []string{"call_member"} chat := setupRequiresAction(ctx, t, db, member.ID, firstUser.OrganizationID, modelConfig.ID, toolName, toolCallIDs) err := memberClient.SubmitToolResults(ctx, chat.ID, codersdk.SubmitToolResultsRequest{ Results: []codersdk.ToolResult{ - {ToolCallID: "call_noaccess", Output: json.RawMessage(`"should fail"`)}, + {ToolCallID: "call_member", Output: json.RawMessage(`"result"`)}, }, }) - requireSDKError(t, err, http.StatusNotFound) + require.NoError(t, err) }) t.Run("ArchivedChat", func(t *testing.T) { @@ -18264,7 +18226,6 @@ func TestChatReadOnlySharedWriteHandlers(t *testing.T) { t, ownerClient.Client, owner.OrganizationID, - rbac.ScopedRoleAgentsAccess(owner.OrganizationID), ) sharedClient = codersdk.NewExperimentalClient(sharedRaw) @@ -18496,7 +18457,6 @@ func TestChatOwnerOnlyWriteHandlers(t *testing.T) { ownerClient.Client, firstUser.OrganizationID, rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID), - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), ) adminClient = codersdk.NewExperimentalClient(orgAdminRaw) return ownerClient, adminClient, chat, db diff --git a/coderd/members.go b/coderd/members.go index 21728c065d546..4bf80b6c91795 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "net/http" + "slices" "github.com/google/uuid" "golang.org/x/xerrors" @@ -510,12 +511,17 @@ func convertOrganizationMembers(ctx context.Context, db database.Store, mems []d roleLookup := make([]database.NameOrganizationPair, 0) for _, m := range mems { + // Stored role arrays may retain retired built-in role names until a + // cleanup migration lands. They grant nothing, so hide them from + // responses to keep role editors from displaying or resubmitting + // them. + activeRoles := slices.DeleteFunc(slices.Clone(m.Roles), rbac.IsRetiredRoleName) converted = append(converted, codersdk.OrganizationMember{ UserID: m.UserID, OrganizationID: m.OrganizationID, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, - Roles: slice.List(m.Roles, func(r string) codersdk.SlimRole { + Roles: slice.List(activeRoles, func(r string) codersdk.SlimRole { // If it is a built-in role, no lookups are needed. rbacRole, err := rbac.RoleByName(rbac.RoleIdentifier{Name: r, OrganizationID: m.OrganizationID}) if err == nil { diff --git a/coderd/members_test.go b/coderd/members_test.go index c2bf219c1ebc2..e83990b6e9157 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -6,12 +6,17 @@ import ( "testing" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" @@ -50,6 +55,143 @@ func TestAddMember(t *testing.T) { }) } +// TestMembersWithRetiredRole verifies that stale grants of retired built-in +// roles, which linger in the database until a cleanup migration lands, do +// not break membership management. +func TestMembersWithRetiredRole(t *testing.T) { + t.Parallel() + + // The raw, unauthorized store is required to seed stale data without + // tripping the assignment validation under test. + db, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{Database: db, Pubsub: ps}) + owner := coderdtest.CreateFirstUser(t, client) + memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitMedium) + + // Seed stale data in the store: the retired agents-access role remains + // in a member's site-wide and org role arrays and in the org's default + // member roles. + _, err := db.UpdateMemberRoles(ctx, database.UpdateMemberRolesParams{ + GrantedRoles: []string{"agents-access"}, + UserID: member.ID, + OrgID: owner.OrganizationID, + }) + require.NoError(t, err) + _, err = db.UpdateUserRoles(ctx, database.UpdateUserRolesParams{ + GrantedRoles: []string{"agents-access"}, + ID: member.ID, + }) + require.NoError(t, err) + + org, err := db.GetOrganizationByID(ctx, owner.OrganizationID) + require.NoError(t, err) + updateOrg := database.UpdateOrganizationParams{ + ID: org.ID, + UpdatedAt: dbtime.Now(), + Name: org.Name, + DisplayName: org.DisplayName, + Description: org.Description, + Icon: org.Icon, + DefaultOrgMemberRoles: append(org.DefaultOrgMemberRoles, "agents-access"), + } + _, err = db.UpdateOrganization(ctx, updateOrg) + require.NoError(t, err) + + // Requests expand the member's stored roles, including the stale grant. + _, err = memberClient.User(ctx, codersdk.Me) + require.NoError(t, err) + + // User creation inserts an organization membership, which validates the + // org's default member roles. The stale default must not fail it. + coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + // Membership responses hide the stale grant so role editors do not + // display or resubmit it. + members, err := client.OrganizationMembers(ctx, owner.OrganizationID) + require.NoError(t, err) + for _, m := range members { + if m.UserID != member.ID { + continue + } + for _, role := range m.Roles { + require.NotEqual(t, "agents-access", role.Name) + } + for _, role := range m.GlobalRoles { + require.NotEqual(t, "agents-access", role.Name) + } + } + + // User payloads hide the stale site-wide grant the same way. + userResp, err := client.User(ctx, member.ID.String()) + require.NoError(t, err) + for _, role := range userResp.Roles { + require.NotEqual(t, "agents-access", role.Name) + } + + // Organization responses hide the stale default role the same way. + orgResp, err := client.Organization(ctx, owner.OrganizationID) + require.NoError(t, err) + require.NotContains(t, orgResp.DefaultOrgMemberRoles, "agents-access") + + // The user-roles endpoint hides the stale grant the same way. + userRoles, err := memberClient.UserRoles(ctx, codersdk.Me) + require.NoError(t, err) + require.NotContains(t, userRoles.Roles, "agents-access") + require.NotContains(t, userRoles.OrganizationRoles[owner.OrganizationID], "agents-access") + + // Restore the defaults so the stale grant is no longer implied and the + // next update must validate its removal. + updateOrg.DefaultOrgMemberRoles = org.DefaultOrgMemberRoles + _, err = db.UpdateOrganization(ctx, updateOrg) + require.NoError(t, err) + + // Role updates validate the removed set, which includes the stale + // grant. The update must succeed and strip the retired role. + updated, err := client.UpdateOrganizationMemberRoles(ctx, owner.OrganizationID, member.ID.String(), codersdk.UpdateRoles{ + Roles: []string{codersdk.RoleOrganizationAuditor}, + }) + require.NoError(t, err) + names := make([]string, 0, len(updated.Roles)) + for _, role := range updated.Roles { + names = append(names, role.Name) + } + require.Contains(t, names, codersdk.RoleOrganizationAuditor) + require.NotContains(t, names, "agents-access") + + // Explicitly granting the retired role again is rejected for both org + // and site scope, so tolerance of stale data cannot be used to store + // fresh grants that a binary rollback would resolve again. + _, err = client.UpdateOrganizationMemberRoles(ctx, owner.OrganizationID, member.ID.String(), codersdk.UpdateRoles{ + Roles: []string{codersdk.RoleOrganizationAuditor, "agents-access"}, + }) + require.ErrorContains(t, err, "retired") + + _, err = client.UpdateUserRoles(ctx, member.ID.String(), codersdk.UpdateRoles{ + Roles: []string{"agents-access"}, + }) + require.ErrorContains(t, err, "retired") + + // Adding the retired role to the org defaults is rejected at the + // authorization boundary, mirroring the explicit grant paths. The + // authorized store is used on purpose, unlike the raw seeding above. + authzdb := dbauthz.New(db, rbac.NewAuthorizer(prometheus.NewRegistry()), testutil.Logger(t), coderdtest.AccessControlStorePointer()) + ownerUser, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + ownerSubject := coderdtest.AuthzUserSubjectWithDB(ctx, t, db, ownerUser) + _, err = authzdb.UpdateOrganization(dbauthz.As(ctx, ownerSubject), database.UpdateOrganizationParams{ + ID: org.ID, + UpdatedAt: dbtime.Now(), + Name: org.Name, + DisplayName: org.DisplayName, + Description: org.Description, + Icon: org.Icon, + DefaultOrgMemberRoles: append(org.DefaultOrgMemberRoles, "agents-access"), + }) + require.ErrorContains(t, err, "retired") +} + func TestDeleteMember(t *testing.T) { t.Parallel() diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 3aceb98323814..0e483d79172bd 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -22,7 +22,6 @@ const ( templateAdmin string = "template-admin" userAdmin string = "user-admin" auditor string = "auditor" - agentsAccess string = "agents-access" // customSiteRole is a placeholder for all custom site roles. // This is used for what roles can assign other roles. // TODO: Make this more dynamic to allow other roles to grant. @@ -143,7 +142,6 @@ func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAd func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} } func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} } func RoleAuditor() RoleIdentifier { return RoleIdentifier{Name: auditor} } -func RoleAgentsAccess() string { return agentsAccess } func RoleOrgAdmin() string { return orgAdmin @@ -203,10 +201,6 @@ func ScopedRoleOrgWorkspaceCreationBan(organizationID uuid.UUID) RoleIdentifier return RoleIdentifier{Name: RoleOrgWorkspaceCreationBan(), OrganizationID: organizationID} } -func ScopedRoleAgentsAccess(organizationID uuid.UUID) RoleIdentifier { - return RoleIdentifier{Name: RoleAgentsAccess(), OrganizationID: organizationID} -} - func ScopedRoleOrgWorkspaceAccess(organizationID uuid.UUID) RoleIdentifier { return RoleIdentifier{Name: RoleOrgWorkspaceAccess(), OrganizationID: organizationID} } @@ -346,9 +340,29 @@ type RoleOptions struct { NoChatSharing bool } +// retiredRoleNames contains retired built-in role names. They stay reserved so +// a custom role cannot take a name that older binaries still resolve as a +// built-in role, which would silently shadow the custom permissions on +// rollback. Stored role arrays and org default role lists may still contain +// these names until a data cleanup migration lands, so role expansion and +// assignment validation treat them as grants of nothing instead of failing. +var retiredRoleNames = map[string]struct{}{ + "agents-access": {}, +} + +// IsRetiredRoleName reports whether name is a retired built-in role name that +// may still appear in stored role arrays. +func IsRetiredRoleName(name string) bool { + _, ok := retiredRoleNames[name] + return ok +} + // ReservedRoleName exists because the database should only allow unique role // names, but some roles are built in. So these names are reserved func ReservedRoleName(name string) bool { + if _, ok := retiredRoleNames[name]; ok { + return true + } _, ok := loadBuiltinRoles()[name] return ok } @@ -729,29 +743,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) { }, } }, - // ActionDelete is intentionally excluded because hard-deletion goes through - // ResourceSystem in dbpurge. - agentsAccess: func(organizationID uuid.UUID) Role { - return Role{ - Identifier: RoleIdentifier{Name: agentsAccess, OrganizationID: organizationID}, - DisplayName: "Coder Agents User", - Site: []Permission{}, - User: []Permission{}, - ByOrgID: map[string]OrgPermissions{ - organizationID.String(): { - Org: []Permission{}, - Member: Permissions(map[string][]policy.Action{ - ResourceChat.Type: { - policy.ActionCreate, - policy.ActionRead, - policy.ActionShare, - policy.ActionUpdate, - }, - }), - }, - }, - } - }, } builtInRoles.Store(&roles) @@ -778,7 +769,6 @@ var assignRoles = map[string]map[string]bool{ userAdmin: true, customSiteRole: true, customOrganizationRole: true, - agentsAccess: true, }, owner: { owner: true, @@ -795,13 +785,11 @@ var assignRoles = map[string]map[string]bool{ userAdmin: true, customSiteRole: true, customOrganizationRole: true, - agentsAccess: true, }, userAdmin: { member: true, orgMember: true, orgWorkspaceAccess: true, - agentsAccess: true, }, orgAdmin: { orgAdmin: true, @@ -812,12 +800,10 @@ var assignRoles = map[string]map[string]bool{ orgWorkspaceCreationBan: true, orgWorkspaceAccess: true, customOrganizationRole: true, - agentsAccess: true, }, orgUserAdmin: { orgMember: true, orgWorkspaceAccess: true, - agentsAccess: true, }, } @@ -1002,6 +988,12 @@ func RoleByName(name RoleIdentifier) (Role, error) { func rolesByNames(roleNames []RoleIdentifier) ([]Role, error) { roles := make([]Role, 0, len(roleNames)) for _, n := range roleNames { + if IsRetiredRoleName(n.Name) { + // Retired role names grant nothing and cannot be re-created as + // custom roles, so stale stored grants are dropped instead of + // failing expansion. + continue + } r, err := RoleByName(n) if err != nil { return nil, xerrors.Errorf("get role permissions: %w", err) @@ -1188,8 +1180,6 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions { }) } - // Chat access requires the agents-access role and is intentionally - // not granted in the floor. memberPerms := Permissions(map[string][]policy.Action{ // Read-self org-member record. ResourceOrganizationMember.Type: {policy.ActionRead}, @@ -1211,6 +1201,14 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions { ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), + + // Hard deletion is authorized through ResourceSystem in dbpurge. + ResourceChat.Type: { + policy.ActionCreate, + policy.ActionRead, + policy.ActionShare, + policy.ActionUpdate, + }, }) if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone { @@ -1258,6 +1256,7 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions { }) } + // Chat permissions are intentionally omitted for service accounts. memberPerms := Permissions(map[string][]policy.Action{ // Read-self org-member record. ResourceOrganizationMember.Type: {policy.ActionRead}, @@ -1268,8 +1267,7 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions { // Service accounts can create and update AI Bridge interceptions // they initiate (dbauthz layer sets WithOwner(InitiatorID)) but - // cannot read them back. Chat access requires the agents-access - // role and is intentionally not granted here. + // cannot read them back. ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate}, // Own session tokens and workspace agent auth keys. diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 060ff175b0525..9029236c3ff3d 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -126,18 +126,26 @@ func TestChatSharingPermissions(t *testing.T) { userID := uuid.NewString() resource := rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(userID) - authorizeAgentsAccessUser := func(t *testing.T) error { + authorizeOrgMember := func(t *testing.T) error { t.Helper() memberRole, err := rbac.RoleByName(rbac.RoleMember()) require.NoError(t, err) - agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID)) - require.NoError(t, err) + perms := rbac.OrgMemberPermissions(rbac.OrgSettings{}) + orgMemberRole := rbac.Role{ + Identifier: rbac.ScopedRoleOrgMember(orgID), + ByOrgID: map[string]rbac.OrgPermissions{ + orgID.String(): { + Org: perms.Org, + Member: perms.Member, + }, + }, + } auth := rbac.NewStrictAuthorizer(prometheus.NewRegistry()) return auth.Authorize(context.Background(), rbac.Subject{ ID: userID, - Roles: rbac.Roles{memberRole, agentsRole}, + Roles: rbac.Roles{memberRole, orgMemberRole}, Scope: rbac.ScopeAll, }, policy.ActionShare, resource) } @@ -149,7 +157,7 @@ func TestChatSharingPermissions(t *testing.T) { memberRole, err := rbac.RoleByName(rbac.RoleMember()) require.NoError(t, err) assert.False(t, permissionGranted(memberRole.Site, target)) - require.NoError(t, authorizeAgentsAccessUser(t)) + require.NoError(t, authorizeOrgMember(t)) }) t.Run("Disabled", func(t *testing.T) { @@ -162,7 +170,7 @@ func TestChatSharingPermissions(t *testing.T) { require.NoError(t, err) assert.True(t, permissionGranted(memberRole.Site, target)) - err = authorizeAgentsAccessUser(t) + err = authorizeOrgMember(t) require.ErrorAs(t, err, &rbac.UnauthorizedError{}) }) } @@ -226,10 +234,12 @@ func TestMemberRolesExcludeWorkspacePerms(t *testing.T) { member := rbac.OrgMemberPermissions(orgSettings).Member require.False(t, hasResource(member, rbac.ResourceWorkspace.Type), "organization-member must not grant workspace permissions") require.True(t, hasResource(member, rbac.ResourceOrganizationMember.Type), "organization-member should grant read-self") + require.True(t, hasResource(member, rbac.ResourceChat.Type), "organization-member should grant chat access") sa := rbac.OrgServiceAccountPermissions(orgSettings).Member require.False(t, hasResource(sa, rbac.ResourceWorkspace.Type), "organization-service-account must not grant workspace permissions") require.True(t, hasResource(sa, rbac.ResourceOrganizationMember.Type), "organization-service-account should grant read-self") + require.False(t, hasResource(sa, rbac.ResourceChat.Type), "organization-service-account must not grant chat access") // The registered organization-workspace-access role is the grant // path for workspace permissions. @@ -288,21 +298,6 @@ func TestRolePermissions(t *testing.T) { orgUserAdmin := authSubject{Name: "org_user_admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgUserAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()} orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()} orgAdminBanWorkspace := authSubject{Name: "org_admin_workspace_ban", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()} - agentsAccessUser := func() authSubject { - memberRole, err := rbac.RoleByName(rbac.RoleMember()) - require.NoError(t, err) - agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID)) - require.NoError(t, err) - return authSubject{ - Name: "agents_access", - Actor: rbac.Subject{ - ID: currentUser.String(), - Roles: rbac.Roles{memberRole, agentsRole}, - Scope: rbac.ScopeAll, - }.WithCachedASTValue(), - } - }() - orgWorkspaceAccessUser := func() authSubject { memberRole, err := rbac.RoleByName(rbac.RoleMember()) require.NoError(t, err) @@ -357,7 +352,7 @@ func TestRolePermissions(t *testing.T) { // requiredSubjects are required to be asserted in each test case. This is // to make sure one is not forgotten. requiredSubjects := []authSubject{ - memberMe, owner, agentsAccessUser, orgWorkspaceAccessUser, + memberMe, owner, orgWorkspaceAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, } @@ -380,7 +375,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin, orgWorkspaceAccessUser}, + true: {owner, memberMe, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin, orgWorkspaceAccessUser}, false: { orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, @@ -393,7 +388,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceUser, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -403,7 +398,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin}, }, }, { @@ -413,7 +408,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -423,7 +418,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, { @@ -433,7 +428,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(policy.WildcardSymbol), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -443,7 +438,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgWorkspaceAccessUser}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, }, }, { @@ -453,7 +448,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgWorkspaceAccessUser}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, }, }, { @@ -462,7 +457,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, { @@ -471,7 +466,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -484,7 +479,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, false: { - memberMe, agentsAccessUser, setOtherOrg, + memberMe, setOtherOrg, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, }, @@ -501,8 +496,7 @@ func TestRolePermissions(t *testing.T) { true: {}, false: { orgAdmin, owner, setOtherOrg, - userAdmin, memberMe, agentsAccessUser, - templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, + userAdmin, memberMe, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace, orgWorkspaceAccessUser, }, @@ -514,7 +508,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -523,7 +517,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTemplate.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgUserAdmin, memberMe, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -534,7 +528,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -545,7 +539,7 @@ func TestRolePermissions(t *testing.T) { true: {owner, templateAdmin}, // Org template admins can only read org scoped files. // File scope is currently not org scoped :cry: - false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, }, }, { @@ -553,7 +547,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, + true: {owner, memberMe, templateAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, userAdmin}, }, }, @@ -563,7 +557,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -572,7 +566,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -581,7 +575,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgWorkspaceAccessUser}, }, }, { @@ -590,7 +584,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -599,7 +593,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -607,7 +601,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + true: {setOtherOrg, setOrgNotMe, owner, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -617,7 +611,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -626,7 +620,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -635,7 +629,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgUserAdmin, userAdmin, templateAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgAuditor, orgTemplateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -643,7 +637,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate}, Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + true: {owner, memberMe, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin}, }, }, @@ -655,7 +649,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, orgWorkspaceAccessUser}, }, }, { @@ -663,7 +657,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, + true: {owner, memberMe, userAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, templateAdmin}, }, }, @@ -673,7 +667,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -682,7 +676,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin}, - false: {memberMe, agentsAccessUser, setOtherOrg, orgWorkspaceAccessUser}, + false: {memberMe, setOtherOrg, orgWorkspaceAccessUser}, }, }, { @@ -694,7 +688,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, agentsAccessUser, orgWorkspaceAccessUser}, + true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, userAdmin}, }, }, @@ -708,7 +702,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -721,7 +715,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgWorkspaceAccessUser}, }, }, { @@ -730,7 +724,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgWorkspaceAccessUser}, }, }, { @@ -739,7 +733,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgWorkspaceAccessUser}, }, }, { @@ -748,7 +742,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {orgAdmin, owner, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor}, }, }, { @@ -757,7 +751,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {orgAdmin, owner, orgWorkspaceAccessUser}, - false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, memberMe, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -766,7 +760,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, userAdmin, owner, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -775,7 +769,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -784,7 +778,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, + false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -793,7 +787,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, // Some admin style resources @@ -803,7 +797,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceLicense, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -812,7 +806,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentStats, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -821,7 +815,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentConfig, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -831,7 +825,7 @@ func TestRolePermissions(t *testing.T) { orgID.String(): {policy.ActionRead}, }), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgAuditor, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin}, + true: {owner, orgAdmin, orgAuditor, orgMemberMe, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin}, false: {setOtherOrg, memberMe, templateAdmin, userAdmin, auditor}, }, }, @@ -841,7 +835,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceMCPServerConfig.WithID(uuid.New()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgAuditor, auditor, orgMemberMe, memberMe, agentsAccessUser, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin}, + false: {setOtherOrg, orgAuditor, auditor, orgMemberMe, memberMe, orgWorkspaceAccessUser, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin}, }, }, { @@ -850,7 +844,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDebugInfo, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -859,7 +853,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceReplicas, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -868,7 +862,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTailnetCoordinator, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -877,7 +871,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAuditLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -886,7 +880,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -895,7 +889,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, }, }, { @@ -904,7 +898,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -913,7 +907,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin}, - false: {orgWorkspaceAccessUser, setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, + false: {orgWorkspaceAccessUser, setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -922,7 +916,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerJobs.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgTemplateAdmin, orgAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -931,7 +925,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceSystem, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -940,7 +934,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -948,7 +942,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -958,7 +952,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppSecret, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -967,7 +961,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppCodeToken, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -976,7 +970,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -984,7 +978,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -994,7 +988,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceBuildOrchestration.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1004,7 +998,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser, owner}, + true: {orgWorkspaceAccessUser, memberMe, owner}, false: { userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, @@ -1021,7 +1015,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin, + orgWorkspaceAccessUser, memberMe, userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin, @@ -1035,8 +1029,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1054,7 +1047,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin, + orgWorkspaceAccessUser, memberMe, templateAdmin, orgUserAdmin, userAdmin, orgAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, otherOrgAdmin, @@ -1067,7 +1060,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceWebpushSubscription.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + true: {owner, memberMe, orgWorkspaceAccessUser}, false: {orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin}, }, }, @@ -1079,7 +1072,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin, orgAdmin, otherOrgAdmin, orgUserAdmin, otherOrgUserAdmin}, false: { - memberMe, agentsAccessUser, templateAdmin, + memberMe, templateAdmin, orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, orgWorkspaceAccessUser, @@ -1093,8 +1086,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin}, false: { - userAdmin, memberMe, agentsAccessUser, - orgAuditor, orgUserAdmin, + userAdmin, memberMe, orgAuditor, orgUserAdmin, otherOrgAuditor, otherOrgUserAdmin, orgWorkspaceAccessUser, }, @@ -1107,7 +1099,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, otherOrgAdmin, orgWorkspaceAccessUser}, false: { - memberMe, agentsAccessUser, userAdmin, templateAdmin, + memberMe, userAdmin, templateAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, }, @@ -1119,7 +1111,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceCryptoKey, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1130,7 +1122,7 @@ func TestRolePermissions(t *testing.T) { true: {owner, orgAdmin, orgUserAdmin, userAdmin}, false: { otherOrgAdmin, - memberMe, agentsAccessUser, templateAdmin, + memberMe, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, orgWorkspaceAccessUser, @@ -1146,7 +1138,7 @@ func TestRolePermissions(t *testing.T) { false: { orgAdmin, orgUserAdmin, otherOrgAdmin, - memberMe, agentsAccessUser, templateAdmin, + memberMe, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, orgWorkspaceAccessUser, @@ -1160,8 +1152,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1176,8 +1167,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1191,7 +1181,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceConnectionLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, // Only the user themselves can access their own secrets — no one else. @@ -1200,7 +1190,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceUserSecret.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + true: {memberMe, orgWorkspaceAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1215,7 +1205,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + true: {owner, memberMe, orgWorkspaceAccessUser}, false: { orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1228,7 +1218,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser}, + true: {memberMe, orgWorkspaceAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1244,8 +1234,7 @@ func TestRolePermissions(t *testing.T) { true: {}, false: { owner, - memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1259,7 +1248,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser}, + true: {orgWorkspaceAccessUser, owner, memberMe}, false: { orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, @@ -1276,8 +1265,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, auditor}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1295,8 +1283,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1313,8 +1300,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1332,8 +1318,7 @@ func TestRolePermissions(t *testing.T) { true: {}, false: { owner, - orgWorkspaceAccessUser, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1345,7 +1330,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceBoundaryUsage, AuthorizeMap: map[bool][]hasAuthSubjects{ - false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {owner, setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1353,7 +1338,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceAiSeat, AuthorizeMap: map[bool][]hasAuthSubjects{ - false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {owner, setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1362,18 +1347,17 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAiModelPrice, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { - // Boundary logs: members can create logs they own (user-scoped). - // memberMe and agentsAccessUser have ID == currentUser, so they - // match the resource owner. Other subjects have different IDs. + // Both allowed subjects use currentUser as their ID, matching the + // resource owner. Name: "BoundaryLogCreate", Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceBoundaryLog.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser}, + true: {orgWorkspaceAccessUser, memberMe}, false: { owner, orgAdmin, otherOrgAdmin, @@ -1393,8 +1377,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, false: { - orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, owner, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, auditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1410,8 +1393,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, false: { - orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, owner, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, auditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1426,8 +1408,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, auditor}, false: { - orgWorkspaceAccessUser, memberMe, agentsAccessUser, - orgAdmin, otherOrgAdmin, + orgWorkspaceAccessUser, memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1439,8 +1420,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, agentsAccessUser}, - false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, + true: {owner, orgAdmin, orgMemberMe}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -1448,8 +1429,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionShare}, Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, agentsAccessUser}, - false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, + true: {owner, orgAdmin, orgMemberMe}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -1458,7 +1439,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -1468,7 +1449,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, auditor, orgAuditor}, false: { - memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser, + memberMe, orgMemberMe, orgWorkspaceAccessUser, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, otherOrgAdmin, otherOrgAuditor, @@ -1482,7 +1463,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, false: { - memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser, + memberMe, orgMemberMe, orgWorkspaceAccessUser, auditor, orgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, @@ -1645,7 +1626,6 @@ func TestListRoles(t *testing.T) { fmt.Sprintf("organization-template-admin:%s", orgID.String()), fmt.Sprintf("organization-workspace-creation-ban:%s", orgID.String()), fmt.Sprintf("organization-workspace-access:%s", orgID.String()), - fmt.Sprintf("agents-access:%s", orgID.String()), }, orgRoleNames) } @@ -1824,3 +1804,31 @@ func TestDBPurgeBoundaryLogDelete(t *testing.T) { rbac.ResourceBoundaryLog) require.Error(t, err, "DBPurge must not read boundary logs") } + +// TestRetiredRoleNames verifies retired built-in role names stay reserved and +// that stale stored grants of them expand to nothing instead of failing. +func TestRetiredRoleNames(t *testing.T) { + t.Parallel() + + const retiredName = "agents-access" + orgID := uuid.New() + + require.True(t, rbac.IsRetiredRoleName(retiredName)) + // Retired names stay reserved so custom roles cannot shadow them. + require.True(t, rbac.ReservedRoleName(retiredName)) + + // Retired names do not resolve to a role... + _, err := rbac.RoleByName(rbac.RoleIdentifier{Name: retiredName}) + require.Error(t, err) + + // ...but stored role arrays may still contain them, so expansion drops + // them instead of failing. + roles, err := rbac.RoleIdentifiers{ + {Name: retiredName}, + {Name: retiredName, OrganizationID: orgID}, + rbac.ScopedRoleOrgAuditor(orgID), + }.Expand() + require.NoError(t, err) + require.Len(t, roles, 1) + require.Equal(t, rbac.ScopedRoleOrgAuditor(orgID), roles[0].Identifier) +} diff --git a/coderd/rbac/rolestore/rolestore.go b/coderd/rbac/rolestore/rolestore.go index 233df7ae527f0..145a6c27fdc06 100644 --- a/coderd/rbac/rolestore/rolestore.go +++ b/coderd/rbac/rolestore/rolestore.go @@ -82,6 +82,14 @@ func Expand(ctx context.Context, db database.Store, names []rbac.RoleIdentifier) roles := make([]rbac.Role, 0, len(names)) for _, name := range names { + if rbac.IsRetiredRoleName(name.Name) { + // Retired built-in role names may linger in stored role arrays + // until a cleanup migration lands. They grant nothing and stay + // reserved, so skip them instead of falling through to the + // custom-role lookup, which could resurrect an unrelated + // pre-reservation custom role with the same name. + continue + } // Remove any built in roles expanded, err := rbac.RoleByName(name) if err == nil { diff --git a/coderd/rbac/rolestore/rolestore_test.go b/coderd/rbac/rolestore/rolestore_test.go index 78a2c3de62654..1d35b90ff0fe7 100644 --- a/coderd/rbac/rolestore/rolestore_test.go +++ b/coderd/rbac/rolestore/rolestore_test.go @@ -46,6 +46,30 @@ func TestExpandCustomRoleRoles(t *testing.T) { require.Len(t, roles, 1, "role found") } +func TestExpandRetiredRoleName(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + + org := dbgen.Organization(t, db, database.Organization{}) + + // Simulate a custom role that took the name before it became a + // reserved built-in role name. Expanding a stored grant of the + // retired name must not resurrect the custom role. + dbgen.CustomRole(t, db, database.CustomRole{ + Name: "agents-access", + OrganizationID: uuid.NullUUID{ + UUID: org.ID, + Valid: true, + }, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + roles, err := rolestore.Expand(ctx, db, []rbac.RoleIdentifier{{Name: "agents-access", OrganizationID: org.ID}}) + require.NoError(t, err) + require.Empty(t, roles, "retired role names expand to nothing") +} + func TestPrefetchCustomRoles(t *testing.T) { t.Parallel() diff --git a/coderd/roles.go b/coderd/roles.go index 500ada46e46dc..db6009c53de26 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -101,6 +101,13 @@ func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role, customR } for _, role := range customRoles { + // A custom role that predates the reservation of a retired + // built-in name can no longer be granted, so do not offer it. + // Custom-role management surfaces still list it so it can be + // deleted. + if rbac.IsRetiredRoleName(role.Name) { + continue + } canAssign := rbac.CanAssignRole(actorRoles, rbac.CustomSiteRole()) if role.RoleIdentifier().IsOrgRole() { canAssign = rbac.CanAssignRole(actorRoles, rbac.CustomOrganizationRole(role.OrganizationID.UUID)) diff --git a/coderd/users.go b/coderd/users.go index 8dae291a5bdbd..b5dbf97686928 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1767,8 +1767,11 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { } // TODO: Replace this with "GetAuthorizationUserRoles" + // 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. resp := codersdk.UserRoles{ - Roles: user.RBACRoles, + Roles: slices.DeleteFunc(slices.Clone(user.RBACRoles), rbac.IsRetiredRoleName), OrganizationRoles: make(map[uuid.UUID][]string), } @@ -1787,7 +1790,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { } for _, mem := range memberships { - resp.OrganizationRoles[mem.OrganizationMember.OrganizationID] = mem.OrganizationMember.Roles + resp.OrganizationRoles[mem.OrganizationMember.OrganizationID] = slices.DeleteFunc(slices.Clone(mem.OrganizationMember.Roles), rbac.IsRetiredRoleName) } httpapi.Write(ctx, rw, http.StatusOK, resp) diff --git a/coderd/x/chatd/forced_mcp_test.go b/coderd/x/chatd/forced_mcp_test.go index 4f8e6d35d084d..97d131e951252 100644 --- a/coderd/x/chatd/forced_mcp_test.go +++ b/coderd/x/chatd/forced_mcp_test.go @@ -123,13 +123,6 @@ func TestCreateChat_ForceOnMCPServerEnforced(t *testing.T) { cfg.AIBridgeTransportFactory = chatAIGatewayTransportFactoryPointer(chattest.NewMockAIBridgeTransport(t, openAIURL)) }) - // The seeded member needs agents-access to create a chat. - _, err := db.UpdateMemberRoles(dbauthz.AsSystemRestricted(ctx), database.UpdateMemberRolesParams{ - GrantedRoles: []string{rbac.RoleAgentsAccess()}, - UserID: user.ID, - OrgID: org.ID, - }) - require.NoError(t, err) ownerSubject, _, err := httpmw.UserRBACSubject(dbauthz.AsSystemRestricted(ctx), db, user.ID, rbac.ScopeAll) require.NoError(t, err) ownerCtx := dbauthz.As(ctx, ownerSubject) diff --git a/codersdk/rbacroles.go b/codersdk/rbacroles.go index 71b82c6340d78..ea87127a39520 100644 --- a/codersdk/rbacroles.go +++ b/codersdk/rbacroles.go @@ -7,7 +7,10 @@ const ( RoleTemplateAdmin string = "template-admin" RoleUserAdmin string = "user-admin" RoleAuditor string = "auditor" - RoleAgentsAccess string = "agents-access" + // Deprecated: the agents-access role was removed. Coder Agents chat + // access is part of the organization-member permission floor, and + // servers without this built-in role reject assigning it. + RoleAgentsAccess string = "agents-access" RoleOrganizationAdmin string = "organization-admin" RoleOrganizationMember string = "organization-member" diff --git a/docs/ai-coder/agents/getting-started.md b/docs/ai-coder/agents/getting-started.md index c79aa5880f58c..87de3ba8f6f18 100644 --- a/docs/ai-coder/agents/getting-started.md +++ b/docs/ai-coder/agents/getting-started.md @@ -20,11 +20,10 @@ Before you begin, confirm the following: - **Access to configure models** in each organization where you configure models. The **Organization Admin** role and the **Owner** role include this access. A custom role with model configuration access also works. -- **Coder Agents User role** assigned to each user who needs to interact with Coder Agents. - This role is granted **per organization**. Owners and organization admins can - assign it from **Admin settings** > **Organizations** > _[your organization]_ > - **Members**. See [Grant Coder Agents User](#step-2-grant-coder-agents-user) - below. +- **Organization membership** for each user who uses Coder Agents. + Users also need read access to at least one model in the organization. + New models are shared with the whole organization by default; to restrict who can use Coder Agents, narrow the model access lists. + See [Manage model permissions](./models.md#manage-model-permissions). ## Step 1: Configure an LLM provider and model @@ -60,65 +59,7 @@ Detailed instructions for each provider and model option are in the > Start with a single frontier model to validate your setup before adding > additional providers. -## Step 2: Grant Coder Agents User - -The **Coder Agents User** role controls which users can interact with Coder -Agents. The role is assigned **per organization**, so a user must be granted -it in each organization where they need access. Members do not have it by -default. - -Owners always have full access and do not need the role. Repeat the following -steps for each user who needs access in each organization. - -**Dashboard (individual):** - -1. Open **Admin settings** > **Organizations** in the Coder dashboard, then - select the organization where you want to grant access. -1. The **Members** tab opens by default. Find the user in the table. -1. Click the **Roles** cell for that user to open the role editor. -1. Toggle on **Coder Agents User** and save. - -> [!TIP] -> If your deployment has multiple organizations, repeat this for each -> organization where the user needs access. - -**CLI (bulk, per organization):** - -Granting the role via CLI is org-scoped. The `edit-roles` command **replaces** -the member's full set of org roles, so include every role you want them to -keep. To grant `agents-access` to a single user while preserving their -existing org roles: - -```sh -ORG="my-org" -USER="alice" -ROLES=$(coder organizations members list -O "$ORG" -o json \ - | jq -r --arg user "$USER" \ - '.[] | select(.username == $user) | [.roles[].name, "agents-access"] - | unique | join(" ")') -# shellcheck disable=SC2086 -coder organizations members edit-roles "$USER" -O "$ORG" $ROLES -``` - -To grant the role to every member of an organization while preserving their -existing roles: - -```sh -ORG="my-org" -coder organizations members list -O "$ORG" -o json \ - | jq -c '.[] | {user_id, roles: [.roles[].name]}' \ - | while read -r row; do - user_id=$(echo "$row" | jq -r '.user_id') - roles=$(echo "$row" | jq -r '(.roles + ["agents-access"]) | unique | join(" ")') - # shellcheck disable=SC2086 - coder organizations members edit-roles "$user_id" -O "$ORG" $roles - done -``` - -You can also set the organization with the `CODER_ORGANIZATION` environment -variable instead of `-O`. - -## Step 3: Start your first Coder Agent +## Step 2: Start your first Coder Agent 1. Go to the **Agents** page in the Coder dashboard. 1. Select a model from the dropdown (your default will be pre-selected). diff --git a/docs/ai-coder/agents/models.md b/docs/ai-coder/agents/models.md index 1eaefc880307a..a3a8b17c416c1 100644 --- a/docs/ai-coder/agents/models.md +++ b/docs/ai-coder/agents/models.md @@ -141,6 +141,10 @@ Create, update, delete, and share permissions control their corresponding action Members with model share permission can let members and groups in the selected organization use the model. +Model access lists control who can use Coder Agents. +All organization members except service accounts hold chat permissions, but a member without read access to at least one model in the organization can't use the feature. +New models grant read access to the whole organization by default. + 1. Navigate to **Admin settings** > **AI** > **Models**. 2. Select the organization that owns the model. 3. Select the model. diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 29b5d6d2d2c45..63a67aed3f5f2 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -94,11 +94,6 @@ The agent loop runs in the control plane, and developers work through the dashbo Not to be confused with a [workspace agent](#workspace-agent) or the [`coder_agent`](#coder_agent) resource. Refer to [Coder Agents](../ai-coder/agents/index.md). -### Coder Agents User - -The per-organization role that a member needs to use [Coder Agents](#coder-agents). -Refer to [Coder Agents](../ai-coder/agents/index.md). - ### Coder CLI The single `coder` binary used for admin and user operations. diff --git a/enterprise/coderd/exp_chats_test.go b/enterprise/coderd/exp_chats_test.go index 6e1b301a4cb08..791960e93e4bd 100644 --- a/enterprise/coderd/exp_chats_test.go +++ b/enterprise/coderd/exp_chats_test.go @@ -1118,11 +1118,10 @@ func TestCreateChatUsesOrganizationLocalModel(t *testing.T) { }) require.NoError(t, err) - // Create a member with agents-access in both orgs. + // Create a member in both orgs. memberClientRaw, member := coderdtest.CreateAnotherUser( t, client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), - rbac.ScopedRoleAgentsAccess(secondOrg.ID), + rbac.ScopedRoleOrgMember(secondOrg.ID), ) memberClient := codersdk.NewExperimentalClient(memberClientRaw) // Create a chat in the non-default org. @@ -1190,12 +1189,11 @@ func TestCreateChatCrossOrgModelConfigRejected(t *testing.T) { secondOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) - // A member with agents-access in both orgs still cannot bind a chat in - // the second org to the default org's config. + // A member of both orgs still cannot bind a chat in the second org to + // the default org's config. memberClientRaw, _ := coderdtest.CreateAnotherUser( t, client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), - rbac.ScopedRoleAgentsAccess(secondOrg.ID), + rbac.ScopedRoleOrgMember(secondOrg.ID), ) memberClient := codersdk.NewExperimentalClient(memberClientRaw) @@ -1251,11 +1249,10 @@ func TestListChats_OrgAdminOnlySeesOwnChats(t *testing.T) { }) require.NoError(t, err) - // Create a member with agents-access in both orgs. + // Create a member in both orgs. memberClientRaw, _ := coderdtest.CreateAnotherUser( t, client, firstUser.OrganizationID, - rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID), - rbac.ScopedRoleAgentsAccess(secondOrg.ID), + rbac.ScopedRoleOrgMember(secondOrg.ID), ) memberExp := codersdk.NewExperimentalClient(memberClientRaw) // Member creates a chat in the second org. @@ -1271,10 +1268,9 @@ func TestListChats_OrgAdminOnlySeesOwnChats(t *testing.T) { require.NoError(t, err) require.Equal(t, secondOrg.ID, memberChat.OrganizationID) - // Create an org admin in the second org with agents access. adminClientRaw, _ := coderdtest.CreateAnotherUser( t, client, firstUser.OrganizationID, - rbac.ScopedRoleOrgAdmin(secondOrg.ID), rbac.ScopedRoleAgentsAccess(secondOrg.ID), + rbac.ScopedRoleOrgAdmin(secondOrg.ID), ) adminExp := codersdk.NewExperimentalClient(adminClientRaw) diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 318138c0b92f3..c4ea6607bf0c6 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -205,8 +205,10 @@ func (api *API) deleteOrgRole(rw http.ResponseWriter, r *http.Request) { rolename := chi.URLParam(r, "roleName") - // Catch requests that try to delete system roles. - if !validOrganizationRoleRequest(ctx, codersdk.CustomRoleRequest{Name: rolename}, rw) { + // Catch requests that try to delete system roles. Retired built-in + // names skip this: they stay reserved for creation and updates, but a + // custom role that predates the reservation must remain deletable. + if !rbac.IsRetiredRoleName(rolename) && !validOrganizationRoleRequest(ctx, codersdk.CustomRoleRequest{Name: rolename}, rw) { return } diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index e2cc4df5bb215..1def3e0846c0c 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -11,7 +11,10 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" @@ -330,6 +333,36 @@ func TestCustomOrganizationRole(t *testing.T) { require.ErrorContains(t, err, "Resource not found") }) + t.Run("DeleteRetiredName", func(t *testing.T) { + t.Parallel() + db, ps := dbtestutil.NewDB(t) + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{Database: db, Pubsub: ps}, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + }, + }, + }) + + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) + ctx := testutil.Context(t, testutil.WaitMedium) + + // A custom role that took the name before it became a reserved + // retired built-in name. It cannot be created or updated through + // the API anymore, but it must remain deletable. + dbgen.CustomRole(t, db, database.CustomRole{ + Name: "agents-access", + OrganizationID: uuid.NullUUID{ + UUID: first.OrganizationID, + Valid: true, + }, + }) + + err := orgAdmin.DeleteOrganizationRole(ctx, first.OrganizationID, "agents-access") + require.NoError(t, err) + }) + t.Run("Delete", func(t *testing.T) { t.Parallel() owner, first := coderdenttest.New(t, &coderdenttest.Options{ @@ -506,7 +539,6 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: false, {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: false, }), }, { @@ -541,7 +573,6 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: true, }), }, { @@ -576,7 +607,6 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: true, }), }, } diff --git a/site/site_test.go b/site/site_test.go index 3aceee175e38a..476ac29e5b263 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -248,61 +248,54 @@ func TestRenderPermissionsResolvesMe(t *testing.T) { }) require.NoError(t, err) - // GIVEN: a user with the agents-access role at the org level. org := dbgen.Organization(t, db, database.Organization{}) - userWithRole := dbgen.User(t, db, database.User{}) + orgMember := dbgen.User(t, db, database.User{}) dbgen.OrganizationMember(t, db, database.OrganizationMember{ OrganizationID: org.ID, - UserID: userWithRole.ID, - Roles: []string{rbac.RoleAgentsAccess()}, + UserID: orgMember.ID, }) - _, tokenWithRole := dbgen.APIKey(t, db, database.APIKey{ - UserID: userWithRole.ID, + _, orgMemberToken := dbgen.APIKey(t, db, database.APIKey{ + UserID: orgMember.ID, ExpiresAt: time.Now().Add(time.Hour), }) // WHEN: the user loads the page. r := httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionTokenHeader, tokenWithRole) + r.Header.Set(codersdk.SessionTokenHeader, orgMemberToken) rw := httptest.NewRecorder() handler.ServeHTTP(rw, r) require.Equal(t, http.StatusOK, rw.Code) - // THEN: the SSR-rendered permissions include createChat = true - // because the agents-access role grants org-scoped chat create - // permission, and the any_org check picks it up. - var permsWithRole codersdk.AuthorizationResponse - err = json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &permsWithRole) + // The any_org check finds chat permission through the user's membership. + var memberPerms codersdk.AuthorizationResponse + err = json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &memberPerms) require.NoError(t, err) - assert.True(t, permsWithRole["createChat"], "user with agents-access role should have createChat = true") + assert.True(t, memberPerms["createChat"], "organization member should have createChat = true") // THEN: createWorkspace = true because the organization-member role // grants creating a workspace owned by the member, and owner_id "me" // resolves to the requesting user. - assert.True(t, permsWithRole["createWorkspace"], "org member should have createWorkspace = true") + assert.True(t, memberPerms["createWorkspace"], "org member should have createWorkspace = true") - // GIVEN: a user without the agents-access role. - userWithoutRole := dbgen.User(t, db, database.User{}) - _, tokenWithoutRole := dbgen.APIKey(t, db, database.APIKey{ - UserID: userWithoutRole.ID, + userWithoutMembership := dbgen.User(t, db, database.User{}) + _, tokenWithoutMembership := dbgen.APIKey(t, db, database.APIKey{ + UserID: userWithoutMembership.ID, ExpiresAt: time.Now().Add(time.Hour), }) // WHEN: the user loads the page. r = httptest.NewRequest("GET", "/", nil) - r.Header.Set(codersdk.SessionTokenHeader, tokenWithoutRole) + r.Header.Set(codersdk.SessionTokenHeader, tokenWithoutMembership) rw = httptest.NewRecorder() handler.ServeHTTP(rw, r) require.Equal(t, http.StatusOK, rw.Code) - // THEN: createChat = false because the member role does not - // grant chat permissions. - var permsWithoutRole codersdk.AuthorizationResponse - err = json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &permsWithoutRole) + var permsWithoutMembership codersdk.AuthorizationResponse + err = json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &permsWithoutMembership) require.NoError(t, err) - assert.False(t, permsWithoutRole["createChat"], "user without agents-access role should have createChat = false") + assert.False(t, permsWithoutMembership["createChat"], "user without an organization membership should have createChat = false") // THEN: createWorkspace = false because the user belongs to no // organization, so the any_org check has no memberships to satisfy it. - assert.False(t, permsWithoutRole["createWorkspace"], "user without an org membership should have createWorkspace = false") + assert.False(t, permsWithoutMembership["createWorkspace"], "user without an org membership should have createWorkspace = false") // GIVEN: an org member whose only membership carries the // workspace-creation ban role. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4502fd9eac4d1..7d113ada4755e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -8323,6 +8323,9 @@ export interface Role { // From codersdk/rbacroles.go /** * Ideally these roles would be generated from the rbac/roles.go package. + * @deprecated the agents-access role was removed. Coder Agents chat + * access is part of the organization-member permission floor, and + * servers without this built-in role reject assigning it. */ export const RoleAgentsAccess = "agents-access"; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index ba5a6821d3501..02c58753df948 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -274,7 +274,7 @@ export const ForSingleOrgOSSAdmin: Story = { }, }; -export const ForMember: Story = { +export const ForUserWithoutOrganization: Story = { args: { user: MockUserMember, adminPermissions: {}, @@ -315,7 +315,7 @@ export const ForMemberWithModelAccess: Story = { }, }; -export const ForMemberWithAgentsAccess: Story = { +export const ForMember: Story = { args: { user: MockUserMember, adminPermissions: {}, diff --git a/site/src/modules/roles/RoleSelector.stories.tsx b/site/src/modules/roles/RoleSelector.stories.tsx index 3f14e7fb0a9d9..417a8a846db82 100644 --- a/site/src/modules/roles/RoleSelector.stories.tsx +++ b/site/src/modules/roles/RoleSelector.stories.tsx @@ -2,7 +2,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { action } from "storybook/actions"; import { assignableRole, - MockAgentsAccessRole, MockAuditorRole, MockOrganizationAdminRole, MockOrganizationAuditorRole, @@ -80,7 +79,6 @@ const orgMemberRoles = [ assignableRole(MockOrganizationUserAdminRole, true), assignableRole(MockOrganizationTemplateAdminRole, true), assignableRole(MockOrganizationAuditorRole, true), - assignableRole(MockAgentsAccessRole, true), assignableRole(MockWorkspaceCreationBanRole, true), ]; @@ -94,7 +92,7 @@ export const WithAdditionalImpliedRoles: Story = { args: { availableRoles: orgMemberRoles, additionalImpliedRoles: [ - assignableRole(MockAgentsAccessRole, true), + assignableRole(MockWorkspaceCreationBanRole, true), assignableRole(MockOrganizationAuditorRole, true), ], }, @@ -105,7 +103,7 @@ const mixedBuiltInAndCustomRoles = [ assignableRole(MockOrganizationUserAdminRole, true), assignableRole(MockOrganizationTemplateAdminRole, true), { ...assignableRole(MockOrganizationAuditorRole, true), built_in: false }, - { ...assignableRole(MockAgentsAccessRole, true), built_in: false }, + { ...assignableRole(MockWorkspaceCreationBanRole, true), built_in: false }, ]; export const WithDisabledReason: Story = { diff --git a/site/src/modules/roles/index.ts b/site/src/modules/roles/index.ts index 222ecee8002c9..55246db38539f 100644 --- a/site/src/modules/roles/index.ts +++ b/site/src/modules/roles/index.ts @@ -10,7 +10,6 @@ export const roleDescriptions: Record = { "user-admin": "User admin can manage all users and groups.", "template-admin": "Template admin can manage all templates and workspaces.", auditor: "Auditor can access the audit logs.", - "agents-access": "Grants access to Coder Agents chat.", "organization-admin": "Organization admin can manage all resources within this organization.", "organization-user-admin": @@ -53,7 +52,6 @@ const roleNamesByAccessLevel: readonly string[] = [ "organization-template-admin", "auditor", "organization-auditor", - "agents-access", "member", "organization-member", ]; diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx index 03c60fd5d5fe0..b00152a758913 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx @@ -1253,7 +1253,9 @@ export const SettingsViewCoderAgentsLink: Story = { await waitFor(() => { expect( - screen.getByText(/Configure deployment-wide Coder Agents capabilities/), + screen.getByText( + /organization model choices and deployment-wide Coder Agents capabilities/, + ), ).toBeInTheDocument(); }); }, diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index ab078bd8e4f02..ff8bf8beabb55 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -1380,7 +1380,7 @@ export const HookDenied: Story = { }, }; -export const ForbiddenErrorWithRole: Story = { +export const ForbiddenErrorWithPermission: Story = { args: { ...defaultArgs, canCreateChat: true, @@ -1388,14 +1388,10 @@ export const ForbiddenErrorWithRole: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - // The friendly "role required" alert must NOT appear because the - // user has the agents-access role. await expect( canvas.queryByText("Permission required"), ).not.toBeInTheDocument(); - // The generic ErrorAlert should surface the real backend message. await expect(canvas.getByText("Forbidden.")).toBeInTheDocument(); - // The textbox should remain enabled since the user has the role. const textbox = canvas.getByRole("textbox"); await waitFor(() => expect(textbox).not.toHaveAttribute("aria-disabled", "true"), @@ -1541,7 +1537,6 @@ export const RestrictedMultiOrganizationUser: Story = { MockDefaultOrganization, MockOrganization2, ]); - // Model agents-access: "me" supplies the owner for member-scoped chat:create. spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => Object.fromEntries( Object.entries(checks).map(([id, check]) => [ @@ -2014,7 +2009,7 @@ export const EmptyPermittedSetPreservesStoredWorkspace: Story = { revocablePermissions[MockOrganization2.id] = false; await revocableQueryClient?.invalidateQueries(); - await canvas.findByText(/don't have permission/i); + await canvas.findByText(/don't have permission to use Coder Agents/i); revocablePermissions[MockOrganization2.id] = true; await revocableQueryClient?.invalidateQueries(); @@ -2233,7 +2228,7 @@ export const OrgChangeConfirmation: Story = { }, }; -export const ForbiddenNoAgentsRole: Story = { +export const ForbiddenNoOrganizationAccess: Story = { args: { ...defaultArgs, canCreateChat: false, @@ -2242,6 +2237,9 @@ export const ForbiddenNoAgentsRole: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByText("Permission required")).toBeInTheDocument(); + await expect( + canvas.getByText(/don't have permission to use Coder Agents/), + ).toBeInTheDocument(); await expect( canvas.getByRole("link", { name: /View Docs/ }), ).toBeInTheDocument(); @@ -2293,7 +2291,9 @@ export const PermittedOrgsResolvesToEmpty: Story = { const canvas = within(canvasElement); await waitFor( () => { - expect(canvas.getByText(/don't have permission/i)).toBeInTheDocument(); + expect( + canvas.getByText(/don't have permission to use Coder Agents/i), + ).toBeInTheDocument(); }, { timeout: 3000 }, ); @@ -2367,11 +2367,6 @@ export const PermittedOrgsResolvesToSubset: Story = { }, }; -/** - * Member-scoped roles like agents-access grant chat:create only on - * chats the user owns, so the per-org check must carry owner context - * for the picker to render. - */ export const MemberScopedPermissionsShowOrgPicker: Story = { parameters: { showOrganizations: true, diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index b7e8340079b56..adbc031edc98f 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -194,8 +194,7 @@ export const AgentCreateForm: FC = ({ ); const permittedOrgsQuery = useQuery({ ...permittedOrganizations({ - // agents-access grants chat:create only at member scope. "me" is - // replaced with the caller ID so that permission can match. + // "me" resolves to the caller ID for owner-scoped permissions. object: { resource_type: "chat", owner_id: "me" }, action: "create", }), diff --git a/site/src/pages/AgentsPage/components/ChatAccessDeniedAlert.tsx b/site/src/pages/AgentsPage/components/ChatAccessDeniedAlert.tsx index e1a580e4ffdbf..a61aed981089c 100644 --- a/site/src/pages/AgentsPage/components/ChatAccessDeniedAlert.tsx +++ b/site/src/pages/AgentsPage/components/ChatAccessDeniedAlert.tsx @@ -5,9 +5,7 @@ import { Link } from "#/components/Link/Link"; import { docs } from "#/utils/docs"; export const ChatAccessDeniedAlert: FC = () => { - const docsLink = docs( - "/ai-coder/agents/getting-started#step-3-grant-coder-agents-user", - ); + const docsLink = docs("/ai-coder/agents/getting-started"); return ( { Permission required You don't have permission to use Coder Agents. Contact your Coder - administrator for access. Refresh this page after access has been - granted.{" "} + administrator, then refresh this page.{" "} View Docs diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx index b9b16b97d7d7b..3edbbc2b6ba8d 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx @@ -33,17 +33,6 @@ const mockOrgRoles: AssignableRoles[] = [ assignable: true, built_in: true, }, - { - name: "agents-access", - display_name: "Agents Access", - organization_id: MockOrganization.id, - site_permissions: [], - organization_permissions: [], - organization_member_permissions: [], - user_permissions: [], - assignable: true, - built_in: true, - }, ]; const meta: Meta = { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9635059235681..6ddaaf6f12a2a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -377,16 +377,6 @@ export const MockOrganizationAuditorRole: TypesGen.AssignableRoles = { organization_member_permissions: [], }; -export const MockAgentsAccessRole: TypesGen.Role = { - name: "agents-access", - display_name: "Coder Agents User", - site_permissions: [], - user_permissions: [], - organization_id: MockOrganization.id, - organization_permissions: [], - organization_member_permissions: [], -}; - export const MockRoleWithOrgPermissions: TypesGen.AssignableRoles = { name: "my-role-1", display_name: "My Role 1",