feat: enable Coder Agents for organization members (#28186)

## Summary

- make Coder Agents available to organization members without a separate
built-in role
- remove the obsolete role from backend and frontend role surfaces
- update authorization, UI, documentation, and regression coverage

## Database note

This PR intentionally ships no migration so it backports cleanly across
diverged migration numbers. Stale `agents-access` grants may remain in
user role arrays and org default member roles; the retired name is
treated as a grant of nothing (role expansion drops it, assignment
validation ignores it, and the name stays reserved). A follow-up PR will
add the data cleanup migration.

## Validation

- `make gen`
- `make lint`
- repository pre-commit checks
- targeted RBAC, migration, chat API, enterprise, site, Storybook, and
race tests
- `git diff --check`

## Rollback note

Rolling back the deployment restores the previous behavior directly: no
data changed, and older binaries still resolve any lingering
`agents-access` grants.

> Mux created this pull request on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-08-26 13:33:13 +02:00
committed by GitHub
parent 9dda7e7a1b
commit 845790e652
33 changed files with 597 additions and 474 deletions
+14 -5
View File
@@ -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),
}
}
+37
View File
@@ -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.
+3 -9
View File
@@ -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"})
+2 -2
View File
@@ -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{
+1 -2
View File
@@ -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)
},
+65 -105
View File
@@ -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
+7 -1
View File
@@ -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 {
+142
View File
@@ -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()
+36 -38
View File
@@ -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.
+148 -140
View File
@@ -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)
}
+8
View File
@@ -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 {
+24
View File
@@ -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()
+7
View File
@@ -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))
+5 -2
View File
@@ -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)
-7
View File
@@ -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)
+4 -1
View File
@@ -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"
+5 -64
View File
@@ -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).
+4
View File
@@ -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.
-5
View File
@@ -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.
+8 -12
View File
@@ -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)
+4 -2
View File
@@ -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
}
+33 -3
View File
@@ -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,
}),
},
}
+18 -25
View File
@@ -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.
+3
View File
@@ -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";
@@ -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: {},
@@ -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 = {
-2
View File
@@ -10,7 +10,6 @@ export const roleDescriptions: Record<string, string> = {
"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",
];
@@ -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();
});
},
@@ -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,
@@ -194,8 +194,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
);
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",
}),
@@ -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 (
<Alert
@@ -21,8 +19,7 @@ export const ChatAccessDeniedAlert: FC = () => {
<AlertTitle>Permission required</AlertTitle>
<AlertDescription>
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.{" "}
<Link href={docsLink} target="_blank" rel="noreferrer">
View Docs
</Link>
@@ -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<typeof CustomRolesPageView> = {
-10
View File
@@ -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",