diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c3a64a346c..d39f0e9bce 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2811,7 +2811,15 @@ func (q *querier) GetDERPMeshKey(ctx context.Context) (string, error) { } func (q *querier) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { + // Any user who can read chat resources can read the default + // model config, since model resolution is required to create + // a chat. This avoids gating on ResourceDeploymentConfig + // which regular members lack. + act, ok := ActorFromContext(ctx) + if !ok { + return database.ChatModelConfig{}, ErrNoActor + } + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(act.ID)); err != nil { return database.ChatModelConfig{}, err } return q.db.GetDefaultChatModelConfig(ctx) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index d7c01e5089..e97acd4a04 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -631,7 +631,7 @@ func (s *MethodTestSuite) TestChats() { s.Run("GetDefaultChatModelConfig", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { config := testutil.Fake(s.T(), faker, database.ChatModelConfig{}) dbm.EXPECT().GetDefaultChatModelConfig(gomock.Any()).Return(config, nil).AnyTimes() - check.Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(config) + check.Asserts(rbac.ResourceChat.WithOwner(testActorID.String()), policy.ActionRead).Returns(config) })) s.Run("GetChatModelConfigs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { configA := testutil.Fake(s.T(), faker, database.ChatModelConfig{}) diff --git a/coderd/database/migrations/000457_chat_access_role.down.sql b/coderd/database/migrations/000457_chat_access_role.down.sql new file mode 100644 index 0000000000..4a2bfb767a --- /dev/null +++ b/coderd/database/migrations/000457_chat_access_role.down.sql @@ -0,0 +1,4 @@ +-- Remove 'agents-access' from all users who have it. +UPDATE users +SET rbac_roles = array_remove(rbac_roles, 'agents-access') +WHERE 'agents-access' = ANY(rbac_roles); diff --git a/coderd/database/migrations/000457_chat_access_role.up.sql b/coderd/database/migrations/000457_chat_access_role.up.sql new file mode 100644 index 0000000000..e672fe3c64 --- /dev/null +++ b/coderd/database/migrations/000457_chat_access_role.up.sql @@ -0,0 +1,5 @@ +-- Grant 'agents-access' to every user who has ever created a chat. +UPDATE users +SET rbac_roles = array_append(rbac_roles, 'agents-access') +WHERE id IN (SELECT DISTINCT owner_id FROM chats) + AND NOT ('agents-access' = ANY(rbac_roles)); diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 19f1a40755..723fd66ae2 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -877,3 +877,149 @@ func TestMigration000387MigrateTaskWorkspaces(t *testing.T) { require.NoError(t, err) require.Equal(t, 0, antCount, "antagonist workspaces (deleted and regular) should not be migrated") } + +func TestMigration000457ChatAccessRole(t *testing.T) { + t.Parallel() + + const migrationVersion = 457 + + sqlDB := testSQLDB(t) + + // Migrate up to the migration before the one that grants + // agents-access roles. + next, err := migrations.Stepper(sqlDB) + require.NoError(t, err) + for { + version, more, err := next() + require.NoError(t, err) + if !more { + t.Fatalf("migration %d not found", migrationVersion) + } + if version == migrationVersion-1 { + break + } + } + + ctx := testutil.Context(t, testutil.WaitSuperLong) + + // Define test users. + userWithChat := uuid.New() // Has a chat, no agents-access role. + userAlreadyHasRole := uuid.New() // Has a chat and already has agents-access. + userNoChat := uuid.New() // No chat at all. + userWithChatAndRoles := uuid.New() // Has a chat and other existing roles. + + now := time.Now().UTC().Truncate(time.Microsecond) + + // We need a chat_provider and chat_model_config for the chats FK. + providerID := uuid.New() + modelConfigID := uuid.New() + + tx, err := sqlDB.BeginTx(ctx, nil) + require.NoError(t, err) + defer tx.Rollback() + + fixtures := []struct { + query string + args []any + }{ + // Insert test users with varying rbac_roles. + { + `INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + []any{userWithChat, "user-with-chat", "chat@test.com", []byte{}, now, now, "active", pq.StringArray{}, "password"}, + }, + { + `INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + []any{userAlreadyHasRole, "user-already-has-role", "already@test.com", []byte{}, now, now, "active", pq.StringArray{"agents-access"}, "password"}, + }, + { + `INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + []any{userNoChat, "user-no-chat", "nochat@test.com", []byte{}, now, now, "active", pq.StringArray{}, "password"}, + }, + { + `INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + []any{userWithChatAndRoles, "user-with-roles", "roles@test.com", []byte{}, now, now, "active", pq.StringArray{"template-admin"}, "password"}, + }, + // Insert a chat provider and model config for the chats FK. + { + `INSERT INTO chat_providers (id, provider, display_name, api_key, enabled, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + []any{providerID, "openai", "OpenAI", "", true, now, now}, + }, + { + `INSERT INTO chat_model_configs (id, provider, model, display_name, enabled, context_limit, compression_threshold, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + []any{modelConfigID, "openai", "gpt-4", "GPT 4", true, 100000, 70, now, now}, + }, + // Insert chats for users A, B, and D (not C). + { + `INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6)`, + []any{uuid.New(), userWithChat, modelConfigID, "Chat A", now, now}, + }, + { + `INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6)`, + []any{uuid.New(), userAlreadyHasRole, modelConfigID, "Chat B", now, now}, + }, + { + `INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6)`, + []any{uuid.New(), userWithChatAndRoles, modelConfigID, "Chat D", now, now}, + }, + } + + for i, f := range fixtures { + _, err := tx.ExecContext(ctx, f.query, f.args...) + require.NoError(t, err, "fixture %d", i) + } + require.NoError(t, tx.Commit()) + + // Run the migration. + version, _, err := next() + require.NoError(t, err) + require.EqualValues(t, migrationVersion, version) + + // Helper to get rbac_roles for a user. + getRoles := func(t *testing.T, userID uuid.UUID) []string { + t.Helper() + var roles pq.StringArray + err := sqlDB.QueryRowContext(ctx, + "SELECT rbac_roles FROM users WHERE id = $1", userID, + ).Scan(&roles) + require.NoError(t, err) + return roles + } + + // Verify: user with chat gets agents-access. + roles := getRoles(t, userWithChat) + require.Contains(t, roles, "agents-access", + "user with chat should get agents-access") + + // Verify: user who already had agents-access has no duplicate. + roles = getRoles(t, userAlreadyHasRole) + count := 0 + for _, r := range roles { + if r == "agents-access" { + count++ + } + } + require.Equal(t, 1, count, + "user who already had agents-access should not get a duplicate") + + // Verify: user without chat does NOT get agents-access. + roles = getRoles(t, userNoChat) + require.NotContains(t, roles, "agents-access", + "user without chat should not get agents-access") + + // Verify: user with chat and existing roles gets agents-access + // appended while preserving existing roles. + roles = getRoles(t, userWithChatAndRoles) + require.Contains(t, roles, "agents-access", + "user with chat and other roles should get agents-access") + require.Contains(t, roles, "template-admin", + "existing roles should be preserved") +} diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index fa254fb627..4e5bb8a101 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1251,8 +1251,12 @@ func TestGetAuthorizedChats(t *testing.T) { owner := dbgen.User(t, db, database.User{ RBACRoles: []string{rbac.RoleOwner().String()}, }) - member := dbgen.User(t, db, database.User{}) - secondMember := dbgen.User(t, db, database.User{}) + member := dbgen.User(t, db, database.User{ + RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()}, + }) + secondMember := dbgen.User(t, db, database.User{ + RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()}, + }) // Create FK dependencies: a chat provider and model config. ctx := testutil.Context(t, testutil.WaitMedium) @@ -1407,7 +1411,9 @@ 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{}) + paginationUser := dbgen.User(t, db, database.User{ + RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()}, + }) for i := range 7 { _, err := db.InsertChat(ctx, database.InsertChatParams{ OwnerID: paginationUser.ID, diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index fed53e62f6..73c9a1f0f1 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -393,6 +393,11 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) + if !api.Authorize(r, policy.ActionCreate, rbac.ResourceChat.WithOwner(apiKey.UserID.String())) { + httpapi.Forbidden(rw) + return + } + var req codersdk.CreateChatRequest if !httpapi.Read(ctx, rw, r, &req) { return @@ -498,6 +503,10 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { }) return } + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Failed to create chat.", Detail: err.Error(), @@ -616,6 +625,10 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) { EndDate: endDate, }) if err != nil { + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } httpapi.InternalServerError(rw, err) return } @@ -626,6 +639,10 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) { EndDate: endDate, }) if err != nil { + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } httpapi.InternalServerError(rw, err) return } @@ -636,6 +653,10 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) { EndDate: endDate, }) if err != nil { + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } httpapi.InternalServerError(rw, err) return } diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index f4b210537e..81c4e03965 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -194,10 +194,15 @@ func TestPostChats(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) client := newChatClient(t) - user := coderdtest.CreateFirstUser(t, client.Client) + firstUser := coderdtest.CreateFirstUser(t, client.Client) modelConfig := createChatModelConfig(t, client) - chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{ + // 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.RoleAgentsAccess()) + memberClient := codersdk.NewExperimentalClient(memberClientRaw) + + chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ Content: []codersdk.ChatInputPart{ { Type: codersdk.ChatInputPartTypeText, @@ -208,7 +213,7 @@ func TestPostChats(t *testing.T) { require.NoError(t, err) require.NotEqual(t, uuid.Nil, chat.ID) - require.Equal(t, user.UserID, chat.OwnerID) + require.Equal(t, member.ID, chat.OwnerID) require.Equal(t, modelConfig.ID, chat.LastModelConfigID) require.Equal(t, "hello from chats route tests", chat.Title) require.Equal(t, codersdk.ChatStatusPending, chat.Status) @@ -218,9 +223,9 @@ func TestPostChats(t *testing.T) { require.NotNil(t, chat.RootChatID) require.Equal(t, chat.ID, *chat.RootChatID) - chatResult, err := client.GetChat(ctx, chat.ID) + chatResult, err := memberClient.GetChat(ctx, chat.ID) require.NoError(t, err) - messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil) + messagesResult, err := memberClient.GetChatMessages(ctx, chat.ID, nil) require.NoError(t, err) require.Equal(t, chat.ID, chatResult.ID) @@ -240,6 +245,29 @@ func TestPostChats(t *testing.T) { require.True(t, foundUserMessage) }) + t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + client := newChatClient(t) + firstUser := coderdtest.CreateFirstUser(t, client.Client) + _ = createChatModelConfig(t, client) + + // Member without agents-access should be denied. + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + memberClient := codersdk.NewExperimentalClient(memberClientRaw) + + _, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{ + Content: []codersdk.ChatInputPart{ + { + Type: codersdk.ChatInputPartTypeText, + Text: "this should fail", + }, + }, + }) + requireSDKError(t, err, http.StatusForbidden) + }) + t.Run("HidesSystemPromptMessages", func(t *testing.T) { t.Parallel() @@ -271,7 +299,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) + memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) memberClient := codersdk.NewExperimentalClient(memberClientRaw) workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ @@ -307,6 +335,7 @@ func TestPostChats(t *testing.T) { adminClient.Client, firstUser.OrganizationID, rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID), + rbac.RoleAgentsAccess(), ) orgAdminClient := codersdk.NewExperimentalClient(orgAdminClientRaw) @@ -518,7 +547,7 @@ func TestListChats(t *testing.T) { }) require.NoError(t, err) - memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) memberClient := codersdk.NewExperimentalClient(memberClientRaw) memberDBChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ OwnerID: member.ID, @@ -586,6 +615,32 @@ func TestListChats(t *testing.T) { require.Equal(t, memberChats[0].ID, memberChats[0].DiffStatus.ChatID) }) + t.Run("MemberWithoutAgentsAccess", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + client, db := newChatClientWithDatabase(t) + firstUser := coderdtest.CreateFirstUser(t, client.Client) + modelConfig := createChatModelConfig(t, client) + + // Create a member without agents-access and insert a chat + // owned by them via system context. This verifies the + // RBAC filter actually excludes results rather than + // returning empty because no chats exist. + memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + memberClient := codersdk.NewExperimentalClient(memberClientRaw) + _, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{ + OwnerID: member.ID, + LastModelConfigID: modelConfig.ID, + Title: "member chat", + }) + require.NoError(t, err) + + chats, err := memberClient.ListChats(ctx, nil) + require.NoError(t, err) + require.Empty(t, chats) + }) + t.Run("Unauthenticated", func(t *testing.T) { t.Parallel() @@ -1958,7 +2013,7 @@ func TestGetChat(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChat(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -3530,7 +3585,7 @@ func TestRegenerateChatTitle(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.RegenerateChatTitle(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -3855,7 +3910,7 @@ func TestGetChatDiffStatus(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChat(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -4088,7 +4143,7 @@ func TestGetChatDiffContents(t *testing.T) { }) require.NoError(t, err) - otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, err = otherClient.GetChatDiffContents(ctx, createdChat.ID) requireSDKError(t, err, http.StatusNotFound) @@ -4884,7 +4939,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) + otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleAgentsAccess()) otherClient := codersdk.NewExperimentalClient(otherClientRaw) _, _, err = otherClient.GetChatFile(ctx, uploaded.ID) requireSDKError(t, err, http.StatusNotFound) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index a1005a033d..9d2d6e89a9 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -21,6 +21,7 @@ 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. @@ -142,6 +143,7 @@ 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() RoleIdentifier { return RoleIdentifier{Name: agentsAccess} } func RoleOrgAdmin() string { return orgAdmin @@ -316,7 +318,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { denyPermissions..., ), User: append( - allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception), + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception, ResourceChat), Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -402,6 +404,21 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ByOrgID: map[string]OrgPermissions{}, }.withCachedRegoValue() + agentsAccessRole := Role{ + Identifier: RoleAgentsAccess(), + DisplayName: "Use Coder Agents", + Site: []Permission{}, + User: Permissions(map[string][]policy.Action{ + ResourceChat.Type: { + policy.ActionCreate, + policy.ActionRead, + policy.ActionUpdate, + policy.ActionDelete, + }, + }), + ByOrgID: map[string]OrgPermissions{}, + }.withCachedRegoValue() + builtInRoles = map[string]func(orgID uuid.UUID) Role{ // admin grants all actions to all resources. owner: func(_ uuid.UUID) Role { @@ -428,6 +445,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) { return userAdminRole }, + // agentsAccess grants all actions on chat resources owned + // by the user. Without this role, members cannot create + // or interact with chats. + agentsAccess: func(_ uuid.UUID) Role { + return agentsAccessRole + }, + // orgAdmin returns a role with all actions allows in a given // organization scope. orgAdmin: func(organizationID uuid.UUID) Role { @@ -600,6 +624,7 @@ var assignRoles = map[string]map[string]bool{ userAdmin: true, customSiteRole: true, customOrganizationRole: true, + agentsAccess: true, }, owner: { owner: true, @@ -615,10 +640,12 @@ var assignRoles = map[string]map[string]bool{ userAdmin: true, customSiteRole: true, customOrganizationRole: true, + agentsAccess: true, }, userAdmin: { - member: true, - orgMember: true, + member: true, + orgMember: true, + agentsAccess: true, }, orgAdmin: { orgAdmin: true, @@ -854,13 +881,20 @@ func SiteBuiltInRoles() []Role { for _, roleF := range builtInRoles { // Must provide some non-nil uuid to filter out org roles. role := roleF(uuid.New()) - if !role.Identifier.IsOrgRole() { + if !role.Identifier.IsOrgRole() && role.Identifier != RoleAgentsAccess() { roles = append(roles, role) } } return roles } +// AgentsAccessRole returns the agents-access role for use by callers +// that need to include it conditionally (e.g. when the agents +// experiment is enabled). +func AgentsAccessRole() Role { + return builtInRoles[agentsAccess](uuid.Nil) +} + // ChangeRoleSet is a helper function that finds the difference of 2 sets of // roles. When setting a user's new roles, it is equivalent to adding and // removing roles. This set determines the changes, so that the appropriate diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 3421aa408b..1cc7e2cade 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -49,6 +49,11 @@ func TestBuiltInRoles(t *testing.T) { require.NoError(t, r.Valid(), "invalid role") }) } + + t.Run("agents-access", func(t *testing.T) { + t.Parallel() + require.NoError(t, rbac.AgentsAccessRole().Valid(), "invalid role") + }) } // permissionGranted checks whether a permission list contains a @@ -199,6 +204,7 @@ 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 := authSubject{Name: "chat_access", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAgentsAccess()}, Scope: rbac.ScopeAll}.WithCachedASTValue()} setOrgNotMe := authSubjectSet{orgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin} otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()} @@ -210,7 +216,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, + memberMe, owner, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, } @@ -233,7 +239,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin}, + true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin}, false: { orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, @@ -246,7 +252,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceUser, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin}, }, }, { @@ -256,7 +262,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}, - false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, }, }, { @@ -266,7 +272,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace}, - false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -276,7 +282,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, { @@ -286,7 +292,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, userAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin}, }, }, { @@ -296,7 +302,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -306,7 +312,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -315,7 +321,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, { @@ -324,7 +330,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace}, - false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -337,7 +343,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgAdminBanWorkspace}, false: { - memberMe, setOtherOrg, + memberMe, agentsAccessUser, setOtherOrg, templateAdmin, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, }, @@ -354,7 +360,7 @@ func TestRolePermissions(t *testing.T) { true: {}, false: { orgAdmin, owner, setOtherOrg, - userAdmin, memberMe, + userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace, }, @@ -366,7 +372,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, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin}, }, }, { @@ -375,7 +381,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, userAdmin}, + false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, }, }, { @@ -386,7 +392,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, }, }, { @@ -397,7 +403,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, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, }, }, { @@ -405,7 +411,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, templateAdmin}, + true: {owner, memberMe, agentsAccessUser, templateAdmin}, false: {setOtherOrg, setOrgNotMe, userAdmin}, }, }, @@ -415,7 +421,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -424,7 +430,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, templateAdmin, userAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -433,7 +439,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}, + false: {setOtherOrg, memberMe, agentsAccessUser}, }, }, { @@ -442,7 +448,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin}, }, }, { @@ -451,7 +457,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin}, }, }, { @@ -459,7 +465,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {setOtherOrg, setOrgNotMe, owner, memberMe, templateAdmin, userAdmin}, + true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin}, false: {}, }, }, @@ -469,7 +475,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor}, }, }, { @@ -478,7 +484,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -487,7 +493,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgUserAdmin, userAdmin, templateAdmin}, - false: {setOtherOrg, memberMe, orgAuditor, orgTemplateAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin}, }, }, { @@ -495,7 +501,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}, + true: {owner, memberMe, agentsAccessUser}, false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin}, }, }, @@ -507,7 +513,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}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser}, }, }, { @@ -515,7 +521,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, userAdmin}, + true: {owner, memberMe, agentsAccessUser, userAdmin}, false: {setOtherOrg, setOrgNotMe, templateAdmin}, }, }, @@ -525,7 +531,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, templateAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin}, }, }, { @@ -534,7 +540,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, setOtherOrg}, + false: {memberMe, agentsAccessUser, setOtherOrg}, }, }, { @@ -547,7 +553,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor}, - false: {setOtherOrg, memberMe, userAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin}, }, }, { @@ -560,7 +566,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor}, }, }, { @@ -573,7 +579,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, - false: {setOtherOrg, memberMe}, + false: {setOtherOrg, memberMe, agentsAccessUser}, }, }, { @@ -582,7 +588,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}, + false: {setOtherOrg, memberMe, agentsAccessUser}, }, }, { @@ -591,7 +597,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}, + false: {setOtherOrg, memberMe, agentsAccessUser}, }, }, { @@ -600,7 +606,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}, - false: {setOtherOrg, userAdmin, memberMe, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -609,7 +615,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, userAdmin, owner, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin}, }, }, { @@ -618,7 +624,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}, - false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -627,7 +633,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, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor}, }, }, { @@ -636,7 +642,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}, - false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, // Some admin style resources @@ -646,7 +652,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceLicense, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -655,7 +661,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentStats, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -664,7 +670,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentConfig, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -673,7 +679,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDebugInfo, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -682,7 +688,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceReplicas, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -691,7 +697,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTailnetCoordinator, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -700,7 +706,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAuditLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -709,7 +715,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, userAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, }, }, { @@ -718,7 +724,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, - false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, }, }, { @@ -727,7 +733,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin}, - false: {setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -736,7 +742,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerJobs.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgTemplateAdmin, orgAdmin}, - false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -745,7 +751,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceSystem, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -754,7 +760,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -762,7 +768,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, false: {}, }, }, @@ -772,7 +778,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppSecret, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -781,7 +787,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppCodeToken, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -790,7 +796,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -798,7 +804,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, false: {}, }, }, @@ -809,7 +815,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, owner}, + true: {memberMe, agentsAccessUser, owner}, false: { userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, @@ -826,7 +832,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, userAdmin, orgUserAdmin, templateAdmin, + memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin, @@ -840,7 +846,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, + memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -858,7 +864,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, templateAdmin, orgUserAdmin, userAdmin, + memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin, orgAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, otherOrgAdmin, @@ -871,7 +877,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}, + true: {owner, memberMe, agentsAccessUser}, false: {orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin}, }, }, @@ -883,7 +889,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin, orgAdmin, otherOrgAdmin, orgUserAdmin, otherOrgUserAdmin}, false: { - memberMe, templateAdmin, + memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, }, @@ -896,7 +902,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin}, false: { - userAdmin, memberMe, + userAdmin, memberMe, agentsAccessUser, orgAuditor, orgUserAdmin, otherOrgAuditor, otherOrgUserAdmin, }, @@ -909,7 +915,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, otherOrgAdmin}, false: { - memberMe, userAdmin, templateAdmin, + memberMe, agentsAccessUser, userAdmin, templateAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, }, @@ -921,7 +927,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceCryptoKey, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -932,7 +938,7 @@ func TestRolePermissions(t *testing.T) { true: {owner, orgAdmin, orgUserAdmin, userAdmin}, false: { otherOrgAdmin, - memberMe, templateAdmin, + memberMe, agentsAccessUser, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, }, @@ -947,7 +953,7 @@ func TestRolePermissions(t *testing.T) { false: { orgAdmin, orgUserAdmin, otherOrgAdmin, - memberMe, templateAdmin, + memberMe, agentsAccessUser, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, }, @@ -960,7 +966,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, + memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -975,7 +981,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, + memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -989,7 +995,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceConnectionLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, // Only the user themselves can access their own secrets — no one else. @@ -998,7 +1004,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}, + true: {memberMe, agentsAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1014,7 +1020,7 @@ func TestRolePermissions(t *testing.T) { true: {}, false: { owner, - memberMe, + memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1028,7 +1034,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe}, + true: {owner, memberMe, agentsAccessUser}, false: { orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, @@ -1045,7 +1051,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, auditor}, false: { - memberMe, + memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1058,7 +1064,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, templateAdmin, userAdmin}, + false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, { @@ -1066,8 +1072,9 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceChat.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe}, + true: {owner, agentsAccessUser}, false: { + memberMe, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1076,7 +1083,6 @@ func TestRolePermissions(t *testing.T) { }, }, } - // Build coverage set from test case definitions statically, // so we don't need shared mutable state during execution. // This allows subtests to run in parallel. @@ -1217,7 +1223,6 @@ func TestListRoles(t *testing.T) { "user-admin", }, siteRoleNames) - orgID := uuid.New() orgRoles := rbac.OrganizationRoles(orgID) orgRoleNames := make([]string, 0, len(orgRoles)) diff --git a/coderd/roles.go b/coderd/roles.go index 3d27ff666c..4779a85114 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/httpapi" @@ -43,7 +44,16 @@ func (api *API) AssignableSiteRoles(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, rbac.SiteBuiltInRoles(), dbCustomRoles)) + siteRoles := rbac.SiteBuiltInRoles() + // Include the agents-access role only when the agents + // experiment is enabled or this is a dev build, matching + // the RequireExperimentWithDevBypass gate on chat routes. + if api.Experiments.Enabled(codersdk.ExperimentAgents) || buildinfo.IsDev() { + siteRoles = append(siteRoles, rbac.AgentsAccessRole()) + } + + httpapi.Write(ctx, rw, http.StatusOK, + assignableRoles(actorRoles.Roles, siteRoles, dbCustomRoles)) } // assignableOrgRoles returns all org wide roles that can be assigned. diff --git a/codersdk/rbacroles.go b/codersdk/rbacroles.go index 7721eacbd5..c48c5cf95c 100644 --- a/codersdk/rbacroles.go +++ b/codersdk/rbacroles.go @@ -1,12 +1,13 @@ package codersdk -// Ideally this roles would be generated from the rbac/roles.go package. +// Ideally these roles would be generated from the rbac/roles.go package. const ( RoleOwner string = "owner" RoleMember string = "member" RoleTemplateAdmin string = "template-admin" RoleUserAdmin string = "user-admin" RoleAuditor string = "auditor" + RoleAgentsAccess string = "agents-access" RoleOrganizationAdmin string = "organization-admin" RoleOrganizationMember string = "organization-member" diff --git a/docs/ai-coder/agents/early-access.md b/docs/ai-coder/agents/early-access.md index e305aa7755..afee5fce77 100644 --- a/docs/ai-coder/agents/early-access.md +++ b/docs/ai-coder/agents/early-access.md @@ -65,6 +65,9 @@ Once the server restarts with the experiment enabled: 1. Navigate to the **Agents** page in the Coder dashboard. 1. Open **Admin** settings and configure at least one LLM provider and model. See [Models](./models.md) for detailed setup instructions. +1. Grant the **Use Coder Agents** role to users who need to create chats. + Go to **Admin** > **Users**, click the roles icon next to each user, + and enable **Use Coder Agents**. 1. Developers can then start a new chat from the Agents page. ## Licensing and availability diff --git a/docs/ai-coder/agents/getting-started.md b/docs/ai-coder/agents/getting-started.md index cc820f3a2f..0989d2b7f8 100644 --- a/docs/ai-coder/agents/getting-started.md +++ b/docs/ai-coder/agents/getting-started.md @@ -24,6 +24,9 @@ Before you begin, confirm the following: for the agent to select when provisioning workspaces. - **Admin access** to the Coder deployment for enabling the experiment and configuring providers. +- **Use Coder Agents role** assigned to each user who needs to create or use chats. + Owners can assign this from **Admin** > **Users**. See + [Grant Use Coder Agents](#step-3-grant-use-coder-agents) below. ## Step 1: Enable the experiment @@ -69,7 +72,23 @@ 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 3: Start your first chat +## Step 3: Grant Use Coder Agents + +The **Use Coder Agents** role controls which users can create and use chats. +Members do not have Use Coder Agents by default. + +1. Go to **Admin** > **Users** in the Coder dashboard. +1. Click the roles icon next to the user you want to grant access to. +1. Enable the **Use Coder Agents** role and save. + +Repeat for each user who needs access. Owners always have full access +and do not need the role. + +> [!NOTE] +> Users who created chats before this role was introduced are +> automatically granted the role during upgrade. + +## Step 4: Start your first chat 1. Go to the **Agents** page in the Coder dashboard. 1. Select a model from the dropdown (your default will be pre-selected). diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index e8b6ae9218..40411bb5b2 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -452,7 +452,13 @@ func TestCustomOrganizationRole(t *testing.T) { func TestListRoles(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentAgents)} + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, @@ -487,6 +493,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleAuditor}: false, {Name: codersdk.RoleTemplateAdmin}: false, {Name: codersdk.RoleUserAdmin}: false, + {Name: codersdk.RoleAgentsAccess}: false, }), }, { @@ -520,6 +527,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleAuditor}: false, {Name: codersdk.RoleTemplateAdmin}: false, {Name: codersdk.RoleUserAdmin}: false, + {Name: codersdk.RoleAgentsAccess}: false, }), }, { @@ -553,6 +561,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleAuditor}: true, {Name: codersdk.RoleTemplateAdmin}: true, {Name: codersdk.RoleUserAdmin}: true, + {Name: codersdk.RoleAgentsAccess}: true, }), }, { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 61941fb84b..75a175b3f7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -5925,56 +5925,62 @@ export interface Role { // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. + */ +export const RoleAgentsAccess = "agents-access"; + +// From codersdk/rbacroles.go +/** + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleAuditor = "auditor"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleMember = "member"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationAdmin = "organization-admin"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationAuditor = "organization-auditor"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationMember = "organization-member"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationTemplateAdmin = "organization-template-admin"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationUserAdmin = "organization-user-admin"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOrganizationWorkspaceCreationBan = "organization-workspace-creation-ban"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleOwner = "owner"; @@ -5993,13 +5999,13 @@ export interface RoleSyncSettings { // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleTemplateAdmin = "template-admin"; // From codersdk/rbacroles.go /** - * Ideally this roles would be generated from the rbac/roles.go package. + * Ideally these roles would be generated from the rbac/roles.go package. */ export const RoleUserAdmin = "user-admin"; diff --git a/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx index 4bad9461dd..3ff32adcbd 100644 --- a/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx @@ -29,6 +29,7 @@ const roleDescriptions: Record = { "user-admin": "User admin can manage all users and groups.", "template-admin": "Template admin can manage all templates and workspaces.", auditor: "Auditor can access the audit logs.", + "agents-access": "Use Coder Agents allows creating and using AI chats.", member: "Everybody is a member. This is a shared and default role for all users.", }; diff --git a/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx index ff5b5be4db..d7ca41fba9 100644 --- a/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx @@ -177,6 +177,7 @@ const roleNamesByAccessLevel: readonly string[] = [ "organization-template-admin", "auditor", "organization-auditor", + "agents-access", ]; function sortRolesByAccessLevel(