mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Generated
+3
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
-11
@@ -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> = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user