From 4c33846f6dd3fda8bf7682486ebb4896274bb741 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 25 Mar 2025 14:18:06 +0200 Subject: [PATCH] chore: add prebuilds system user (#16916) Pre-requisite for https://github.com/coder/coder/pull/16891 Closes https://github.com/coder/internal/issues/515 This PR introduces a new concept of a "system" user. Our data model requires that all workspaces have an owner (a `users` relation), and prebuilds is a feature that will spin up workspaces to be claimed later by actual users - and thus needs to own the workspaces in the interim. Naturally, introducing a change like this touches a few aspects around the codebase and we've taken the approach _default hidden_ here; in other words, queries for users will by default _exclude_ all system users, but there is a flag to ensure they can be displayed. This keeps the changeset relatively small. This user has minimal permissions (it's equivalent to a `member` since it has no roles). It will be associated with the default org in the initial migration, and thereafter we'll need to somehow ensure its membership aligns with templates (which are org-scoped) for which it'll need to provision prebuilds; that's a solution we'll have in a subsequent PR. --------- Signed-off-by: Danny Kopping Co-authored-by: Sas Swart --- cli/server.go | 2 +- coderd/database/dbauthz/dbauthz.go | 33 +++-- coderd/database/dbauthz/dbauthz_test.go | 18 ++- coderd/database/dbauthz/groupsauth_test.go | 5 +- coderd/database/dbgen/dbgen_test.go | 5 +- coderd/database/dbmem/dbmem.go | 65 +++++++-- coderd/database/dbmetrics/querymetrics.go | 25 ++-- coderd/database/dbmock/dbmock.go | 48 +++---- coderd/database/dump.sql | 4 + .../000195_oauth2_provider_codes.up.sql | 4 + .../migrations/000308_system_user.down.sql | 50 +++++++ .../migrations/000308_system_user.up.sql | 57 ++++++++ coderd/database/modelmethods.go | 1 + coderd/database/modelqueries.go | 2 + coderd/database/models.go | 3 + coderd/database/querier.go | 12 +- coderd/database/querier_test.go | 113 ++++++++++++++- coderd/database/queries.sql.go | 131 +++++++++++++----- coderd/database/queries/groupmembers.sql | 27 +++- .../database/queries/organizationmembers.sql | 6 + coderd/database/queries/users.sql | 21 ++- coderd/httpmw/organizationparam.go | 1 + coderd/idpsync/role.go | 2 + coderd/members.go | 1 + coderd/prebuilds/id.go | 5 + coderd/telemetry/telemetry.go | 2 +- coderd/userauth.go | 3 +- coderd/userauth_test.go | 3 +- coderd/users.go | 5 +- coderd/users_test.go | 3 +- docs/admin/security/audit-logs.md | 2 +- enterprise/audit/table.go | 1 + enterprise/coderd/groups.go | 41 ++++-- enterprise/coderd/groups_test.go | 3 +- enterprise/coderd/license/license.go | 2 +- enterprise/coderd/templates.go | 20 ++- enterprise/coderd/templates_test.go | 3 +- enterprise/dbcrypt/cliutil.go | 5 +- 38 files changed, 591 insertions(+), 143 deletions(-) create mode 100644 coderd/database/migrations/000308_system_user.down.sql create mode 100644 coderd/database/migrations/000308_system_user.up.sql create mode 100644 coderd/prebuilds/id.go diff --git a/cli/server.go b/cli/server.go index 0b64cd8aa6..3fefc51357 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1894,7 +1894,7 @@ func getGithubOAuth2ConfigParams(ctx context.Context, db database.Store, vals *c if defaultEligibleNotSet { // nolint:gocritic // User count requires system privileges - userCount, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + userCount, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx), false) if err != nil { return nil, xerrors.Errorf("get user count: %w", err) } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 94c0c7ef62..275ca1fc3c 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1057,13 +1057,13 @@ func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg database.Activi return update(q.log, q.auth, fetch, q.db.ActivityBumpWorkspace)(ctx, arg) } -func (q *querier) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) { +func (q *querier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) { // Although this technically only reads users, only system-related functions should be // allowed to call this. if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } - return q.db.AllUserIDs(ctx) + return q.db.AllUserIDs(ctx, includeSystem) } func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) { @@ -1316,7 +1316,11 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) { - member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams(arg))) + member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{ + OrganizationID: arg.OrganizationID, + UserID: arg.UserID, + IncludeSystem: false, + })) if err != nil { return database.OrganizationMember{}, err } @@ -1502,11 +1506,11 @@ func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Tim return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed) } -func (q *querier) GetActiveUserCount(ctx context.Context) (int64, error) { +func (q *querier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return 0, err } - return q.db.GetActiveUserCount(ctx) + return q.db.GetActiveUserCount(ctx, includeSystem) } func (q *querier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) { @@ -1737,22 +1741,22 @@ func (q *querier) GetGroupByOrgAndName(ctx context.Context, arg database.GetGrou return fetch(q.log, q.auth, q.db.GetGroupByOrgAndName)(ctx, arg) } -func (q *querier) GetGroupMembers(ctx context.Context) ([]database.GroupMember, error) { +func (q *querier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]database.GroupMember, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } - return q.db.GetGroupMembers(ctx) + return q.db.GetGroupMembers(ctx, includeSystem) } -func (q *querier) GetGroupMembersByGroupID(ctx context.Context, id uuid.UUID) ([]database.GroupMember, error) { - return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupID)(ctx, id) +func (q *querier) GetGroupMembersByGroupID(ctx context.Context, arg database.GetGroupMembersByGroupIDParams) ([]database.GroupMember, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupID)(ctx, arg) } -func (q *querier) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) { - if _, err := q.GetGroupByID(ctx, groupID); err != nil { // AuthZ check +func (q *querier) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) { + if _, err := q.GetGroupByID(ctx, arg.GroupID); err != nil { // AuthZ check return 0, err } - memberCount, err := q.db.GetGroupMembersCountByGroupID(ctx, groupID) + memberCount, err := q.db.GetGroupMembersCountByGroupID(ctx, arg) if err != nil { return 0, err } @@ -2530,11 +2534,11 @@ func (q *querier) GetUserByID(ctx context.Context, id uuid.UUID) (database.User, return fetch(q.log, q.auth, q.db.GetUserByID)(ctx, id) } -func (q *querier) GetUserCount(ctx context.Context) (int64, error) { +func (q *querier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return 0, err } - return q.db.GetUserCount(ctx) + return q.db.GetUserCount(ctx, includeSystem) } func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) { @@ -3778,6 +3782,7 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: arg.OrgID, UserID: arg.UserID, + IncludeSystem: false, })) if err != nil { return database.OrganizationMember{}, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 149051bd3b..b280fa8902 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -387,19 +387,25 @@ func (s *MethodTestSuite) TestGroup() { g := dbgen.Group(s.T(), db, database.Group{}) u := dbgen.User(s.T(), db, database.User{}) gm := dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID}) - check.Args(g.ID).Asserts(gm, policy.ActionRead) + check.Args(database.GetGroupMembersByGroupIDParams{ + GroupID: g.ID, + IncludeSystem: false, + }).Asserts(gm, policy.ActionRead) })) s.Run("GetGroupMembersCountByGroupID", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) g := dbgen.Group(s.T(), db, database.Group{}) - check.Args(g.ID).Asserts(g, policy.ActionRead) + check.Args(database.GetGroupMembersCountByGroupIDParams{ + GroupID: g.ID, + IncludeSystem: false, + }).Asserts(g, policy.ActionRead) })) s.Run("GetGroupMembers", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) g := dbgen.Group(s.T(), db, database.Group{}) u := dbgen.User(s.T(), db, database.User{}) dbgen.GroupMember(s.T(), db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID}) - check.Asserts(rbac.ResourceSystem, policy.ActionRead) + check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("System/GetGroups", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) @@ -1681,7 +1687,7 @@ func (s *MethodTestSuite) TestUser() { s.Run("AllUserIDs", s.Subtest(func(db database.Store, check *expects) { a := dbgen.User(s.T(), db, database.User{}) b := dbgen.User(s.T(), db, database.User{}) - check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(slice.New(a.ID, b.ID)) + check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(slice.New(a.ID, b.ID)) })) s.Run("CustomRoles", s.Subtest(func(db database.Store, check *expects) { check.Args(database.CustomRolesParams{}).Asserts(rbac.ResourceAssignRole, policy.ActionRead).Returns([]database.CustomRole{}) @@ -3696,7 +3702,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetActiveUserCount", s.Subtest(func(db database.Store, check *expects) { - check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) + check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) })) s.Run("GetUnexpiredLicenses", s.Subtest(func(db database.Store, check *expects) { check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) @@ -3739,7 +3745,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args(time.Now().Add(time.Hour*-1)).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetUserCount", s.Subtest(func(db database.Store, check *expects) { - check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) + check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) })) s.Run("GetTemplates", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) diff --git a/coderd/database/dbauthz/groupsauth_test.go b/coderd/database/dbauthz/groupsauth_test.go index 04d816629a..a9f26e303d 100644 --- a/coderd/database/dbauthz/groupsauth_test.go +++ b/coderd/database/dbauthz/groupsauth_test.go @@ -147,7 +147,10 @@ func TestGroupsAuth(t *testing.T) { require.Error(t, err, "group read") } - members, err := db.GetGroupMembersByGroupID(actorCtx, group.ID) + members, err := db.GetGroupMembersByGroupID(actorCtx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if tc.ReadMembers { require.NoError(t, err, "member read") require.Len(t, members, tc.MembersExpected, "member count found does not match") diff --git a/coderd/database/dbgen/dbgen_test.go b/coderd/database/dbgen/dbgen_test.go index eec6e90d59..de45f90d91 100644 --- a/coderd/database/dbgen/dbgen_test.go +++ b/coderd/database/dbgen/dbgen_test.go @@ -105,7 +105,10 @@ func TestGenerator(t *testing.T) { gm := dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: g.ID, UserID: u.ID}) exp := []database.GroupMember{gm} - require.Equal(t, exp, must(db.GetGroupMembersByGroupID(context.Background(), g.ID))) + require.Equal(t, exp, must(db.GetGroupMembersByGroupID(context.Background(), database.GetGroupMembersByGroupIDParams{ + GroupID: g.ID, + IncludeSystem: false, + }))) }) t.Run("Organization", func(t *testing.T) { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 56e272c7ba..2596d843ea 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -23,6 +23,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/notifications/types" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -154,6 +155,22 @@ func New() database.Store { panic(xerrors.Errorf("failed to create psk provisioner key: %w", err)) } + q.mutex.Lock() + // We can't insert this user using the interface, because it's a system user. + q.data.users = append(q.data.users, database.User{ + ID: prebuilds.SystemUserID, + Email: "prebuilds@coder.com", + Username: "prebuilds", + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + Status: "active", + LoginType: "none", + HashedPassword: []byte{}, + IsSystem: true, + Deleted: false, + }) + q.mutex.Unlock() + return q } @@ -442,6 +459,7 @@ func convertUsers(users []database.User, count int64) []database.GetUsersRow { Deleted: u.Deleted, LastSeenAt: u.LastSeenAt, Count: count, + IsSystem: u.IsSystem, } } @@ -1554,11 +1572,16 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.Ac return sql.ErrNoRows } -func (q *FakeQuerier) AllUserIDs(_ context.Context) ([]uuid.UUID, error) { +// nolint:revive // It's not a control flag, it's a filter. +func (q *FakeQuerier) AllUserIDs(_ context.Context, includeSystem bool) ([]uuid.UUID, error) { q.mutex.RLock() defer q.mutex.RUnlock() userIDs := make([]uuid.UUID, 0, len(q.users)) for idx := range q.users { + if !includeSystem && q.users[idx].IsSystem { + continue + } + userIDs = append(userIDs, q.users[idx].ID) } return userIDs, nil @@ -2649,12 +2672,17 @@ func (q *FakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time return apiKeys, nil } -func (q *FakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) { +// nolint:revive // It's not a control flag, it's a filter. +func (q *FakeQuerier) GetActiveUserCount(_ context.Context, includeSystem bool) (int64, error) { q.mutex.RLock() defer q.mutex.RUnlock() active := int64(0) for _, u := range q.users { + if !includeSystem && u.IsSystem { + continue + } + if u.Status == database.UserStatusActive && !u.Deleted { active++ } @@ -3390,7 +3418,8 @@ func (q *FakeQuerier) GetGroupByOrgAndName(_ context.Context, arg database.GetGr return database.Group{}, sql.ErrNoRows } -func (q *FakeQuerier) GetGroupMembers(ctx context.Context) ([]database.GroupMember, error) { +//nolint:revive // It's not a control flag, its a filter +func (q *FakeQuerier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]database.GroupMember, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3398,6 +3427,9 @@ func (q *FakeQuerier) GetGroupMembers(ctx context.Context) ([]database.GroupMemb members = append(members, q.groupMembers...) for _, org := range q.organizations { for _, user := range q.users { + if !includeSystem && user.IsSystem { + continue + } members = append(members, database.GroupMemberTable{ UserID: user.ID, GroupID: org.ID, @@ -3420,17 +3452,17 @@ func (q *FakeQuerier) GetGroupMembers(ctx context.Context) ([]database.GroupMemb return groupMembers, nil } -func (q *FakeQuerier) GetGroupMembersByGroupID(ctx context.Context, id uuid.UUID) ([]database.GroupMember, error) { +func (q *FakeQuerier) GetGroupMembersByGroupID(ctx context.Context, arg database.GetGroupMembersByGroupIDParams) ([]database.GroupMember, error) { q.mutex.RLock() defer q.mutex.RUnlock() - if q.isEveryoneGroup(id) { - return q.getEveryoneGroupMembersNoLock(ctx, id), nil + if q.isEveryoneGroup(arg.GroupID) { + return q.getEveryoneGroupMembersNoLock(ctx, arg.GroupID), nil } var groupMembers []database.GroupMember for _, member := range q.groupMembers { - if member.GroupID == id { + if member.GroupID == arg.GroupID { groupMember, err := q.getGroupMemberNoLock(ctx, member.UserID, member.GroupID) if errors.Is(err, errUserDeleted) { continue @@ -3445,8 +3477,8 @@ func (q *FakeQuerier) GetGroupMembersByGroupID(ctx context.Context, id uuid.UUID return groupMembers, nil } -func (q *FakeQuerier) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) { - users, err := q.GetGroupMembersByGroupID(ctx, groupID) +func (q *FakeQuerier) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) { + users, err := q.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams(arg)) if err != nil { return 0, err } @@ -6223,12 +6255,16 @@ func (q *FakeQuerier) GetUserByID(_ context.Context, id uuid.UUID) (database.Use return q.getUserByIDNoLock(id) } -func (q *FakeQuerier) GetUserCount(_ context.Context) (int64, error) { +// nolint:revive // It's not a control flag, it's a filter. +func (q *FakeQuerier) GetUserCount(_ context.Context, includeSystem bool) (int64, error) { q.mutex.RLock() defer q.mutex.RUnlock() existing := int64(0) for _, u := range q.users { + if !includeSystem && u.IsSystem { + continue + } if !u.Deleted { existing++ } @@ -6580,6 +6616,12 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams users = usersFilteredByLastSeen } + if !params.IncludeSystem { + users = slices.DeleteFunc(users, func(u database.User) bool { + return u.IsSystem + }) + } + if params.GithubComUserID != 0 { usersFilteredByGithubComUserID := make([]database.User, 0, len(users)) for i, user := range users { @@ -8933,6 +8975,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam Status: status, RBACRoles: arg.RBACRoles, LoginType: arg.LoginType, + IsSystem: false, } q.users = append(q.users, user) sort.Slice(q.users, func(i, j int) bool { @@ -10091,7 +10134,7 @@ func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params dat var updated []database.UpdateInactiveUsersToDormantRow for index, user := range q.users { - if user.Status == database.UserStatusActive && user.LastSeenAt.Before(params.LastSeenAfter) { + if user.Status == database.UserStatusActive && user.LastSeenAt.Before(params.LastSeenAfter) && !user.IsSystem { q.users[index].Status = database.UserStatusDormant q.users[index].UpdatedAt = params.UpdatedAt updated = append(updated, database.UpdateInactiveUsersToDormantRow{ diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 4d19aa6529..3eb40842e6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -115,9 +116,9 @@ func (m queryMetricsStore) ActivityBumpWorkspace(ctx context.Context, arg databa return r0 } -func (m queryMetricsStore) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) { +func (m queryMetricsStore) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) { start := time.Now() - r0, r1 := m.s.AllUserIDs(ctx) + r0, r1 := m.s.AllUserIDs(ctx, includeSystem) m.queryLatencies.WithLabelValues("AllUserIDs").Observe(time.Since(start).Seconds()) return r0, r1 } @@ -514,9 +515,9 @@ func (m queryMetricsStore) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed return apiKeys, err } -func (m queryMetricsStore) GetActiveUserCount(ctx context.Context) (int64, error) { +func (m queryMetricsStore) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) { start := time.Now() - count, err := m.s.GetActiveUserCount(ctx) + count, err := m.s.GetActiveUserCount(ctx, includeSystem) m.queryLatencies.WithLabelValues("GetActiveUserCount").Observe(time.Since(start).Seconds()) return count, err } @@ -759,23 +760,23 @@ func (m queryMetricsStore) GetGroupByOrgAndName(ctx context.Context, arg databas return group, err } -func (m queryMetricsStore) GetGroupMembers(ctx context.Context) ([]database.GroupMember, error) { +func (m queryMetricsStore) GetGroupMembers(ctx context.Context, includeSystem bool) ([]database.GroupMember, error) { start := time.Now() - r0, r1 := m.s.GetGroupMembers(ctx) + r0, r1 := m.s.GetGroupMembers(ctx, includeSystem) m.queryLatencies.WithLabelValues("GetGroupMembers").Observe(time.Since(start).Seconds()) return r0, r1 } -func (m queryMetricsStore) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]database.GroupMember, error) { +func (m queryMetricsStore) GetGroupMembersByGroupID(ctx context.Context, arg database.GetGroupMembersByGroupIDParams) ([]database.GroupMember, error) { start := time.Now() - users, err := m.s.GetGroupMembersByGroupID(ctx, groupID) + users, err := m.s.GetGroupMembersByGroupID(ctx, arg) m.queryLatencies.WithLabelValues("GetGroupMembersByGroupID").Observe(time.Since(start).Seconds()) return users, err } -func (m queryMetricsStore) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) { +func (m queryMetricsStore) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) { start := time.Now() - r0, r1 := m.s.GetGroupMembersCountByGroupID(ctx, groupID) + r0, r1 := m.s.GetGroupMembersCountByGroupID(ctx, arg) m.queryLatencies.WithLabelValues("GetGroupMembersCountByGroupID").Observe(time.Since(start).Seconds()) return r0, r1 } @@ -1424,9 +1425,9 @@ func (m queryMetricsStore) GetUserByID(ctx context.Context, id uuid.UUID) (datab return user, err } -func (m queryMetricsStore) GetUserCount(ctx context.Context) (int64, error) { +func (m queryMetricsStore) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) { start := time.Now() - count, err := m.s.GetUserCount(ctx) + count, err := m.s.GetUserCount(ctx, includeSystem) m.queryLatencies.WithLabelValues("GetUserCount").Observe(time.Since(start).Seconds()) return count, err } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 3389455562..ac824c9fff 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -103,18 +103,18 @@ func (mr *MockStoreMockRecorder) ActivityBumpWorkspace(ctx, arg any) *gomock.Cal } // AllUserIDs mocks base method. -func (m *MockStore) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) { +func (m *MockStore) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AllUserIDs", ctx) + ret := m.ctrl.Call(m, "AllUserIDs", ctx, includeSystem) ret0, _ := ret[0].([]uuid.UUID) ret1, _ := ret[1].(error) return ret0, ret1 } // AllUserIDs indicates an expected call of AllUserIDs. -func (mr *MockStoreMockRecorder) AllUserIDs(ctx any) *gomock.Call { +func (mr *MockStoreMockRecorder) AllUserIDs(ctx, includeSystem any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllUserIDs", reflect.TypeOf((*MockStore)(nil).AllUserIDs), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllUserIDs", reflect.TypeOf((*MockStore)(nil).AllUserIDs), ctx, includeSystem) } // ArchiveUnusedTemplateVersions mocks base method. @@ -923,18 +923,18 @@ func (mr *MockStoreMockRecorder) GetAPIKeysLastUsedAfter(ctx, lastUsed any) *gom } // GetActiveUserCount mocks base method. -func (m *MockStore) GetActiveUserCount(ctx context.Context) (int64, error) { +func (m *MockStore) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetActiveUserCount", ctx) + ret := m.ctrl.Call(m, "GetActiveUserCount", ctx, includeSystem) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // GetActiveUserCount indicates an expected call of GetActiveUserCount. -func (mr *MockStoreMockRecorder) GetActiveUserCount(ctx any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetActiveUserCount(ctx, includeSystem any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), ctx, includeSystem) } // GetActiveWorkspaceBuildsByTemplateID mocks base method. @@ -1523,48 +1523,48 @@ func (mr *MockStoreMockRecorder) GetGroupByOrgAndName(ctx, arg any) *gomock.Call } // GetGroupMembers mocks base method. -func (m *MockStore) GetGroupMembers(ctx context.Context) ([]database.GroupMember, error) { +func (m *MockStore) GetGroupMembers(ctx context.Context, includeSystem bool) ([]database.GroupMember, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupMembers", ctx) + ret := m.ctrl.Call(m, "GetGroupMembers", ctx, includeSystem) ret0, _ := ret[0].([]database.GroupMember) ret1, _ := ret[1].(error) return ret0, ret1 } // GetGroupMembers indicates an expected call of GetGroupMembers. -func (mr *MockStoreMockRecorder) GetGroupMembers(ctx any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupMembers(ctx, includeSystem any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembers", reflect.TypeOf((*MockStore)(nil).GetGroupMembers), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembers", reflect.TypeOf((*MockStore)(nil).GetGroupMembers), ctx, includeSystem) } // GetGroupMembersByGroupID mocks base method. -func (m *MockStore) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]database.GroupMember, error) { +func (m *MockStore) GetGroupMembersByGroupID(ctx context.Context, arg database.GetGroupMembersByGroupIDParams) ([]database.GroupMember, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupMembersByGroupID", ctx, groupID) + ret := m.ctrl.Call(m, "GetGroupMembersByGroupID", ctx, arg) ret0, _ := ret[0].([]database.GroupMember) ret1, _ := ret[1].(error) return ret0, ret1 } // GetGroupMembersByGroupID indicates an expected call of GetGroupMembersByGroupID. -func (mr *MockStoreMockRecorder) GetGroupMembersByGroupID(ctx, groupID any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupMembersByGroupID(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersByGroupID", reflect.TypeOf((*MockStore)(nil).GetGroupMembersByGroupID), ctx, groupID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersByGroupID", reflect.TypeOf((*MockStore)(nil).GetGroupMembersByGroupID), ctx, arg) } // GetGroupMembersCountByGroupID mocks base method. -func (m *MockStore) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) { +func (m *MockStore) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupMembersCountByGroupID", ctx, groupID) + ret := m.ctrl.Call(m, "GetGroupMembersCountByGroupID", ctx, arg) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // GetGroupMembersCountByGroupID indicates an expected call of GetGroupMembersCountByGroupID. -func (mr *MockStoreMockRecorder) GetGroupMembersCountByGroupID(ctx, groupID any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupMembersCountByGroupID(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersCountByGroupID", reflect.TypeOf((*MockStore)(nil).GetGroupMembersCountByGroupID), ctx, groupID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersCountByGroupID", reflect.TypeOf((*MockStore)(nil).GetGroupMembersCountByGroupID), ctx, arg) } // GetGroups mocks base method. @@ -2978,18 +2978,18 @@ func (mr *MockStoreMockRecorder) GetUserByID(ctx, id any) *gomock.Call { } // GetUserCount mocks base method. -func (m *MockStore) GetUserCount(ctx context.Context) (int64, error) { +func (m *MockStore) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserCount", ctx) + ret := m.ctrl.Call(m, "GetUserCount", ctx, includeSystem) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // GetUserCount indicates an expected call of GetUserCount. -func (mr *MockStoreMockRecorder) GetUserCount(ctx any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserCount(ctx, includeSystem any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCount", reflect.TypeOf((*MockStore)(nil).GetUserCount), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCount", reflect.TypeOf((*MockStore)(nil).GetUserCount), ctx, includeSystem) } // GetUserLatencyInsights mocks base method. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f36a7aeaf3..e1320cf88f 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -854,6 +854,7 @@ CREATE TABLE users ( github_com_user_id bigint, hashed_one_time_passcode bytea, one_time_passcode_expires_at timestamp with time zone, + is_system boolean DEFAULT false NOT NULL, CONSTRAINT one_time_passcode_set CHECK ((((hashed_one_time_passcode IS NULL) AND (one_time_passcode_expires_at IS NULL)) OR ((hashed_one_time_passcode IS NOT NULL) AND (one_time_passcode_expires_at IS NOT NULL)))) ); @@ -867,6 +868,8 @@ COMMENT ON COLUMN users.hashed_one_time_passcode IS 'A hash of the one-time-pass COMMENT ON COLUMN users.one_time_passcode_expires_at IS 'The time when the one-time-passcode expires.'; +COMMENT ON COLUMN users.is_system IS 'Determines if a user is a system user, and therefore cannot login or perform normal actions'; + CREATE VIEW group_members_expanded AS WITH all_members AS ( SELECT group_members.user_id, @@ -892,6 +895,7 @@ CREATE VIEW group_members_expanded AS users.quiet_hours_schedule AS user_quiet_hours_schedule, users.name AS user_name, users.github_com_user_id AS user_github_com_user_id, + users.is_system AS user_is_system, groups.organization_id, groups.name AS group_name, all_members.group_id diff --git a/coderd/database/migrations/000195_oauth2_provider_codes.up.sql b/coderd/database/migrations/000195_oauth2_provider_codes.up.sql index 04333c0ed2..225a110712 100644 --- a/coderd/database/migrations/000195_oauth2_provider_codes.up.sql +++ b/coderd/database/migrations/000195_oauth2_provider_codes.up.sql @@ -43,6 +43,10 @@ AFTER DELETE ON oauth2_provider_app_tokens FOR EACH ROW EXECUTE PROCEDURE delete_deleted_oauth2_provider_app_token_api_key(); +-- This migration has been modified after its initial commit. +-- The new implementation makes the same changes as the original, but +-- takes into account the message in create_migration.sh. This is done +-- to allow the insertion of a user with the "none" login type in later migrations. CREATE TYPE new_logintype AS ENUM ( 'password', 'github', diff --git a/coderd/database/migrations/000308_system_user.down.sql b/coderd/database/migrations/000308_system_user.down.sql new file mode 100644 index 0000000000..69903b13d3 --- /dev/null +++ b/coderd/database/migrations/000308_system_user.down.sql @@ -0,0 +1,50 @@ +DROP VIEW IF EXISTS group_members_expanded; +CREATE VIEW group_members_expanded AS + WITH all_members AS ( + SELECT group_members.user_id, + group_members.group_id + FROM group_members + UNION + SELECT organization_members.user_id, + organization_members.organization_id AS group_id + FROM organization_members + ) + SELECT users.id AS user_id, + users.email AS user_email, + users.username AS user_username, + users.hashed_password AS user_hashed_password, + users.created_at AS user_created_at, + users.updated_at AS user_updated_at, + users.status AS user_status, + users.rbac_roles AS user_rbac_roles, + users.login_type AS user_login_type, + users.avatar_url AS user_avatar_url, + users.deleted AS user_deleted, + users.last_seen_at AS user_last_seen_at, + users.quiet_hours_schedule AS user_quiet_hours_schedule, + users.name AS user_name, + users.github_com_user_id AS user_github_com_user_id, + groups.organization_id, + groups.name AS group_name, + all_members.group_id + FROM ((all_members + JOIN users ON ((users.id = all_members.user_id))) + JOIN groups ON ((groups.id = all_members.group_id))) + WHERE (users.deleted = false); + +COMMENT ON VIEW group_members_expanded IS 'Joins group members with user information, organization ID, group name. Includes both regular group members and organization members (as part of the "Everyone" group).'; + +-- Remove system user from organizations +DELETE FROM organization_members +WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; + +-- Delete user status changes +DELETE FROM user_status_changes +WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; + +-- Delete system user +DELETE FROM users +WHERE id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; + +-- Drop column +ALTER TABLE users DROP COLUMN IF EXISTS is_system; diff --git a/coderd/database/migrations/000308_system_user.up.sql b/coderd/database/migrations/000308_system_user.up.sql new file mode 100644 index 0000000000..c024a9587f --- /dev/null +++ b/coderd/database/migrations/000308_system_user.up.sql @@ -0,0 +1,57 @@ +ALTER TABLE users + ADD COLUMN is_system bool DEFAULT false NOT NULL; + +COMMENT ON COLUMN users.is_system IS 'Determines if a user is a system user, and therefore cannot login or perform normal actions'; + +INSERT INTO users (id, email, username, name, created_at, updated_at, status, rbac_roles, hashed_password, is_system, login_type) +VALUES ('c42fdf75-3097-471c-8c33-fb52454d81c0', 'prebuilds@system', 'prebuilds', 'Prebuilds Owner', now(), now(), + 'active', '{}', 'none', true, 'none'::login_type); + +DROP VIEW IF EXISTS group_members_expanded; +CREATE VIEW group_members_expanded AS + WITH all_members AS ( + SELECT group_members.user_id, + group_members.group_id + FROM group_members + UNION + SELECT organization_members.user_id, + organization_members.organization_id AS group_id + FROM organization_members + ) + SELECT users.id AS user_id, + users.email AS user_email, + users.username AS user_username, + users.hashed_password AS user_hashed_password, + users.created_at AS user_created_at, + users.updated_at AS user_updated_at, + users.status AS user_status, + users.rbac_roles AS user_rbac_roles, + users.login_type AS user_login_type, + users.avatar_url AS user_avatar_url, + users.deleted AS user_deleted, + users.last_seen_at AS user_last_seen_at, + users.quiet_hours_schedule AS user_quiet_hours_schedule, + users.name AS user_name, + users.github_com_user_id AS user_github_com_user_id, + users.is_system AS user_is_system, + groups.organization_id, + groups.name AS group_name, + all_members.group_id + FROM ((all_members + JOIN users ON ((users.id = all_members.user_id))) + JOIN groups ON ((groups.id = all_members.group_id))) + WHERE (users.deleted = false); + +COMMENT ON VIEW group_members_expanded IS 'Joins group members with user information, organization ID, group name. Includes both regular group members and organization members (as part of the "Everyone" group).'; +-- TODO: do we *want* to use the default org here? how do we handle multi-org? +WITH default_org AS (SELECT id + FROM organizations + WHERE is_default = true + LIMIT 1) +INSERT +INTO organization_members (organization_id, user_id, created_at, updated_at) +SELECT default_org.id, + 'c42fdf75-3097-471c-8c33-fb52454d81c0', -- The system user responsible for prebuilds. + NOW(), + NOW() +FROM default_org; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index a9dbc3e530..5b197a0649 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -423,6 +423,7 @@ func ConvertUserRows(rows []GetUsersRow) []User { AvatarURL: r.AvatarURL, Deleted: r.Deleted, LastSeenAt: r.LastSeenAt, + IsSystem: r.IsSystem, } } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index c8c6ec2d96..3c437cde29 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -393,6 +393,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, arg.LastSeenAfter, arg.CreatedBefore, arg.CreatedAfter, + arg.IncludeSystem, arg.GithubComUserID, arg.OffsetOpt, arg.LimitOpt, @@ -422,6 +423,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/models.go b/coderd/database/models.go index 0ff030271d..201a57a4d6 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2610,6 +2610,7 @@ type GroupMember struct { UserQuietHoursSchedule string `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"` UserName string `db:"user_name" json:"user_name"` UserGithubComUserID sql.NullInt64 `db:"user_github_com_user_id" json:"user_github_com_user_id"` + UserIsSystem bool `db:"user_is_system" json:"user_is_system"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` GroupName string `db:"group_name" json:"group_name"` GroupID uuid.UUID `db:"group_id" json:"group_id"` @@ -3192,6 +3193,8 @@ type User struct { HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"` // The time when the one-time-passcode expires. OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"` + // Determines if a user is a system user, and therefore cannot login or perform normal actions + IsSystem bool `db:"is_system" json:"is_system"` } type UserConfig struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 0c4928e7ff..2dc5f4016f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -49,7 +49,7 @@ type sqlcQuerier interface { // We only bump when 5% of the deadline has elapsed. ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error // AllUserIDs returns all UserIDs regardless of user status or deletion. - AllUserIDs(ctx context.Context) ([]uuid.UUID, error) + AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) // Archiving templates is a soft delete action, so is reversible. // Archiving prevents the version from being used and discovered // by listing. @@ -124,7 +124,7 @@ type sqlcQuerier interface { GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) - GetActiveUserCount(ctx context.Context) (int64, error) + GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) // For PG Coordinator HTMLDebug @@ -172,12 +172,12 @@ type sqlcQuerier interface { GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) - GetGroupMembers(ctx context.Context) ([]GroupMember, error) - GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]GroupMember, error) + GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error) + GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error) // Returns the total count of members in a group. Shows the total // count even if the caller does not have read access to ResourceGroupMember. // They only need ResourceGroup read access. - GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) + GetGroupMembersCountByGroupID(ctx context.Context, arg GetGroupMembersCountByGroupIDParams) (int64, error) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) GetHealthSettings(ctx context.Context) (string, error) GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) @@ -309,7 +309,7 @@ type sqlcQuerier interface { GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) - GetUserCount(ctx context.Context) (int64, error) + GetUserCount(ctx context.Context, includeSystem bool) (int64, error) // GetUserLatencyInsights returns the median and 95th percentile connection // latency that users have experienced. The result can be filtered on // template_ids, meaning only user data from workspaces based on those templates diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 837068f1fa..a2d22f9144 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -25,6 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/migrations" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/provisionersdk" @@ -1364,6 +1365,113 @@ func TestUserLastSeenFilter(t *testing.T) { }) } +func TestGetUsers_IncludeSystem(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + includeSystem bool + wantSystemUser bool + }{ + { + name: "include system users", + includeSystem: true, + wantSystemUser: true, + }, + { + name: "exclude system users", + includeSystem: false, + wantSystemUser: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + // Given: a system user + // postgres: introduced by migration coderd/database/migrations/00030*_system_user.up.sql + // dbmem: created in dbmem/dbmem.go + db, _ := dbtestutil.NewDB(t) + other := dbgen.User(t, db, database.User{}) + users, err := db.GetUsers(ctx, database.GetUsersParams{ + IncludeSystem: tt.includeSystem, + }) + require.NoError(t, err) + + // Should always find the regular user + foundRegularUser := false + foundSystemUser := false + + for _, u := range users { + if u.IsSystem { + foundSystemUser = true + require.Equal(t, prebuilds.SystemUserID, u.ID) + } else { + foundRegularUser = true + require.Equalf(t, other.ID.String(), u.ID.String(), "found unexpected regular user") + } + } + + require.True(t, foundRegularUser, "regular user should always be found") + require.Equal(t, tt.wantSystemUser, foundSystemUser, "system user presence should match includeSystem setting") + require.Equal(t, tt.wantSystemUser, len(users) == 2, "should have 2 users when including system user, 1 otherwise") + }) + } +} + +func TestUpdateSystemUser(t *testing.T) { + t.Parallel() + + // TODO (sasswart): We've disabled the protection that prevents updates to system users + // while we reassess the mechanism to do so. Rather than skip the test, we've just inverted + // the assertions to ensure that the behavior is as desired. + // Once we've re-enabeld the system user protection, we'll revert the assertions. + + ctx := testutil.Context(t, testutil.WaitLong) + + // Given: a system user introduced by migration coderd/database/migrations/00030*_system_user.up.sql + db, _ := dbtestutil.NewDB(t) + users, err := db.GetUsers(ctx, database.GetUsersParams{ + IncludeSystem: true, + }) + require.NoError(t, err) + var systemUser database.GetUsersRow + for _, u := range users { + if u.IsSystem { + systemUser = u + } + } + require.NotNil(t, systemUser) + + // When: attempting to update a system user's name. + _, err = db.UpdateUserProfile(ctx, database.UpdateUserProfileParams{ + ID: systemUser.ID, + Name: "not prebuilds", + }) + // Then: the attempt is rejected by a postgres trigger. + // require.ErrorContains(t, err, "Cannot modify or delete system users") + require.NoError(t, err) + + // When: attempting to delete a system user. + err = db.UpdateUserDeletedByID(ctx, systemUser.ID) + // Then: the attempt is rejected by a postgres trigger. + // require.ErrorContains(t, err, "Cannot modify or delete system users") + require.NoError(t, err) + + // When: attempting to update a user's roles. + _, err = db.UpdateUserRoles(ctx, database.UpdateUserRolesParams{ + ID: systemUser.ID, + GrantedRoles: []string{rbac.RoleAuditor().String()}, + }) + // Then: the attempt is rejected by a postgres trigger. + // require.ErrorContains(t, err, "Cannot modify or delete system users") + require.NoError(t, err) +} + func TestUserChangeLoginType(t *testing.T) { t.Parallel() if testing.Short() { @@ -1505,7 +1613,10 @@ func TestWorkspaceQuotas(t *testing.T) { }) // Fetch the 'Everyone' group members - everyoneMembers, err := db.GetGroupMembersByGroupID(ctx, org.ID) + everyoneMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: everyoneGroup.ID, + IncludeSystem: false, + }) require.NoError(t, err) require.ElementsMatch(t, db2sdk.List(everyoneMembers, groupMemberIDs), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 17ab7ef3e3..6f5e5813c1 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1579,11 +1579,16 @@ func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteG } const getGroupMembers = `-- name: GetGroupMembers :many -SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded +SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, organization_id, group_name, group_id FROM group_members_expanded +WHERE CASE + WHEN $1::bool THEN TRUE + ELSE + user_is_system = false + END ` -func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error) { - rows, err := q.db.QueryContext(ctx, getGroupMembers) +func (q *sqlQuerier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error) { + rows, err := q.db.QueryContext(ctx, getGroupMembers, includeSystem) if err != nil { return nil, err } @@ -1607,6 +1612,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error) &i.UserQuietHoursSchedule, &i.UserName, &i.UserGithubComUserID, + &i.UserIsSystem, &i.OrganizationID, &i.GroupName, &i.GroupID, @@ -1625,11 +1631,24 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error) } const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many -SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded WHERE group_id = $1 +SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, organization_id, group_name, group_id +FROM group_members_expanded +WHERE group_id = $1 + -- Filter by system type + AND CASE + WHEN $2::bool THEN TRUE + ELSE + user_is_system = false + END ` -func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]GroupMember, error) { - rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupID, groupID) +type GetGroupMembersByGroupIDParams struct { + GroupID uuid.UUID `db:"group_id" json:"group_id"` + IncludeSystem bool `db:"include_system" json:"include_system"` +} + +func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error) { + rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupID, arg.GroupID, arg.IncludeSystem) if err != nil { return nil, err } @@ -1653,6 +1672,7 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid. &i.UserQuietHoursSchedule, &i.UserName, &i.UserGithubComUserID, + &i.UserIsSystem, &i.OrganizationID, &i.GroupName, &i.GroupID, @@ -1671,14 +1691,27 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid. } const getGroupMembersCountByGroupID = `-- name: GetGroupMembersCountByGroupID :one -SELECT COUNT(*) FROM group_members_expanded WHERE group_id = $1 +SELECT COUNT(*) +FROM group_members_expanded +WHERE group_id = $1 + -- Filter by system type + AND CASE + WHEN $2::bool THEN TRUE + ELSE + user_is_system = false + END ` +type GetGroupMembersCountByGroupIDParams struct { + GroupID uuid.UUID `db:"group_id" json:"group_id"` + IncludeSystem bool `db:"include_system" json:"include_system"` +} + // Returns the total count of members in a group. Shows the total // count even if the caller does not have read access to ResourceGroupMember. // They only need ResourceGroup read access. -func (q *sqlQuerier) GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) { - row := q.db.QueryRowContext(ctx, getGroupMembersCountByGroupID, groupID) +func (q *sqlQuerier) GetGroupMembersCountByGroupID(ctx context.Context, arg GetGroupMembersCountByGroupIDParams) (int64, error) { + row := q.db.QueryRowContext(ctx, getGroupMembersCountByGroupID, arg.GroupID, arg.IncludeSystem) var count int64 err := row.Scan(&count) return count, err @@ -5232,11 +5265,18 @@ WHERE user_id = $2 ELSE true END + -- Filter by system type + AND CASE + WHEN $3::bool THEN TRUE + ELSE + is_system = false + END ` type OrganizationMembersParams struct { OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` UserID uuid.UUID `db:"user_id" json:"user_id"` + IncludeSystem bool `db:"include_system" json:"include_system"` } type OrganizationMembersRow struct { @@ -5253,7 +5293,7 @@ type OrganizationMembersRow struct { // - Use just 'user_id' to get all orgs a user is a member of // - Use both to get a specific org member row func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) { - rows, err := q.db.QueryContext(ctx, organizationMembers, arg.OrganizationID, arg.UserID) + rows, err := q.db.QueryContext(ctx, organizationMembers, arg.OrganizationID, arg.UserID, arg.IncludeSystem) if err != nil { return nil, err } @@ -7866,7 +7906,7 @@ FROM ( -- Select all groups this user is a member of. This will also include -- the "Everyone" group for organizations the user is a member of. - SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded + SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, organization_id, group_name, group_id FROM group_members_expanded WHERE $1 = user_id AND $2 = group_members_expanded.organization_id @@ -11367,11 +11407,12 @@ func (q *sqlQuerier) UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinke const allUserIDs = `-- name: AllUserIDs :many SELECT DISTINCT id FROM USERS + WHERE CASE WHEN $1::bool THEN TRUE ELSE is_system = false END ` // AllUserIDs returns all UserIDs regardless of user status or deletion. -func (q *sqlQuerier) AllUserIDs(ctx context.Context) ([]uuid.UUID, error) { - rows, err := q.db.QueryContext(ctx, allUserIDs) +func (q *sqlQuerier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, allUserIDs, includeSystem) if err != nil { return nil, err } @@ -11400,10 +11441,11 @@ FROM users WHERE status = 'active'::user_status AND deleted = false + AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END ` -func (q *sqlQuerier) GetActiveUserCount(ctx context.Context) (int64, error) { - row := q.db.QueryRowContext(ctx, getActiveUserCount) +func (q *sqlQuerier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) { + row := q.db.QueryRowContext(ctx, getActiveUserCount, includeSystem) var count int64 err := row.Scan(&count) return count, err @@ -11493,7 +11535,7 @@ func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid. const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system FROM users WHERE @@ -11529,13 +11571,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } const getUserByID = `-- name: GetUserByID :one SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system FROM users WHERE @@ -11565,6 +11608,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -11576,10 +11620,11 @@ FROM users WHERE deleted = false + AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END ` -func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { - row := q.db.QueryRowContext(ctx, getUserCount) +func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) { + row := q.db.QueryRowContext(ctx, getUserCount, includeSystem) var count int64 err := row.Scan(&count) return count, err @@ -11587,7 +11632,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { const getUsers = `-- name: GetUsers :many SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, COUNT(*) OVER() AS count + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, COUNT(*) OVER() AS count FROM users WHERE @@ -11658,9 +11703,14 @@ WHERE created_at >= $8 ELSE true END + AND CASE + WHEN $9::bool THEN TRUE + ELSE + is_system = false + END AND CASE - WHEN $9 :: bigint != 0 THEN - github_com_user_id = $9 + WHEN $10 :: bigint != 0 THEN + github_com_user_id = $10 ELSE true END -- End of filters @@ -11669,10 +11719,10 @@ WHERE -- @authorize_filter ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(username) ASC OFFSET $10 + LOWER(username) ASC OFFSET $11 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($11 :: int, 0) + NULLIF($12 :: int, 0) ` type GetUsersParams struct { @@ -11684,6 +11734,7 @@ type GetUsersParams struct { LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` CreatedBefore time.Time `db:"created_before" json:"created_before"` CreatedAfter time.Time `db:"created_after" json:"created_after"` + IncludeSystem bool `db:"include_system" json:"include_system"` GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` @@ -11707,6 +11758,7 @@ type GetUsersRow struct { GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"` HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"` OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"` + IsSystem bool `db:"is_system" json:"is_system"` Count int64 `db:"count" json:"count"` } @@ -11721,6 +11773,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse arg.LastSeenAfter, arg.CreatedBefore, arg.CreatedAfter, + arg.IncludeSystem, arg.GithubComUserID, arg.OffsetOpt, arg.LimitOpt, @@ -11750,6 +11803,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, &i.Count, ); err != nil { return nil, err @@ -11766,7 +11820,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse } const getUsersByIDs = `-- name: GetUsersByIDs :many -SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at FROM users WHERE id = ANY($1 :: uuid [ ]) +SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system FROM users WHERE id = ANY($1 :: uuid [ ]) ` // This shouldn't check for deleted, because it's frequently used @@ -11799,6 +11853,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ); err != nil { return nil, err } @@ -11832,7 +11887,7 @@ VALUES -- if the status passed in is empty, fallback to dormant, which is what -- we were doing before. COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status) - ) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + ) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type InsertUserParams struct { @@ -11880,6 +11935,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -11889,10 +11945,11 @@ UPDATE users SET status = 'dormant'::user_status, - updated_at = $1 + updated_at = $1 WHERE last_seen_at < $2 :: timestamp AND status = 'active'::user_status + AND NOT is_system RETURNING id, email, username, last_seen_at ` @@ -12045,7 +12102,7 @@ SET last_seen_at = $2, updated_at = $3 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserLastSeenAtParams struct { @@ -12075,6 +12132,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -12092,7 +12150,9 @@ SET '':: bytea END WHERE - id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + id = $2 + AND NOT is_system +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserLoginTypeParams struct { @@ -12121,6 +12181,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -12136,7 +12197,7 @@ SET name = $6 WHERE id = $1 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserProfileParams struct { @@ -12176,6 +12237,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -12187,7 +12249,7 @@ SET quiet_hours_schedule = $2 WHERE id = $1 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserQuietHoursScheduleParams struct { @@ -12216,6 +12278,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -12228,7 +12291,7 @@ SET rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[])) WHERE id = $2 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserRolesParams struct { @@ -12257,6 +12320,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } @@ -12268,7 +12332,7 @@ SET status = $2, updated_at = $3 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system ` type UpdateUserStatusParams struct { @@ -12298,6 +12362,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP &i.GithubComUserID, &i.HashedOneTimePasscode, &i.OneTimePasscodeExpiresAt, + &i.IsSystem, ) return i, err } diff --git a/coderd/database/queries/groupmembers.sql b/coderd/database/queries/groupmembers.sql index 4efe9bf488..7de8dbe4e4 100644 --- a/coderd/database/queries/groupmembers.sql +++ b/coderd/database/queries/groupmembers.sql @@ -1,14 +1,35 @@ -- name: GetGroupMembers :many -SELECT * FROM group_members_expanded; +SELECT * FROM group_members_expanded +WHERE CASE + WHEN @include_system::bool THEN TRUE + ELSE + user_is_system = false + END; -- name: GetGroupMembersByGroupID :many -SELECT * FROM group_members_expanded WHERE group_id = @group_id; +SELECT * +FROM group_members_expanded +WHERE group_id = @group_id + -- Filter by system type + AND CASE + WHEN @include_system::bool THEN TRUE + ELSE + user_is_system = false + END; -- name: GetGroupMembersCountByGroupID :one -- Returns the total count of members in a group. Shows the total -- count even if the caller does not have read access to ResourceGroupMember. -- They only need ResourceGroup read access. -SELECT COUNT(*) FROM group_members_expanded WHERE group_id = @group_id; +SELECT COUNT(*) +FROM group_members_expanded +WHERE group_id = @group_id + -- Filter by system type + AND CASE + WHEN @include_system::bool THEN TRUE + ELSE + user_is_system = false + END; -- InsertUserGroupsByName adds a user to all provided groups, if they exist. -- name: InsertUserGroupsByName :exec diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index a92cd681ea..9d570bc1c4 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -22,6 +22,12 @@ WHERE WHEN @user_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN user_id = @user_id ELSE true + END + -- Filter by system type + AND CASE + WHEN @include_system::bool THEN TRUE + ELSE + is_system = false END; -- name: InsertOrganizationMember :one diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 0c29cf723f..c4304cfc3e 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -11,7 +11,9 @@ SET '':: bytea END WHERE - id = @user_id RETURNING *; + id = @user_id + AND NOT is_system +RETURNING *; -- name: GetUserByID :one SELECT @@ -46,7 +48,8 @@ SELECT FROM users WHERE - deleted = false; + deleted = false + AND CASE WHEN @include_system::bool THEN TRUE ELSE is_system = false END; -- name: GetActiveUserCount :one SELECT @@ -54,7 +57,8 @@ SELECT FROM users WHERE - status = 'active'::user_status AND deleted = false; + status = 'active'::user_status AND deleted = false + AND CASE WHEN @include_system::bool THEN TRUE ELSE is_system = false END; -- name: InsertUser :one INSERT INTO @@ -223,6 +227,11 @@ WHERE created_at >= @created_after ELSE true END + AND CASE + WHEN @include_system::bool THEN TRUE + ELSE + is_system = false + END AND CASE WHEN @github_com_user_id :: bigint != 0 THEN github_com_user_id = @github_com_user_id @@ -316,15 +325,17 @@ UPDATE users SET status = 'dormant'::user_status, - updated_at = @updated_at + updated_at = @updated_at WHERE last_seen_at < @last_seen_after :: timestamp AND status = 'active'::user_status + AND NOT is_system RETURNING id, email, username, last_seen_at; -- AllUserIDs returns all UserIDs regardless of user status or deletion. -- name: AllUserIDs :many -SELECT DISTINCT id FROM USERS; +SELECT DISTINCT id FROM USERS + WHERE CASE WHEN @include_system::bool THEN TRUE ELSE is_system = false END; -- name: UpdateUserHashedOneTimePasscode :exec UPDATE diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index 2eba0dcedf..18938ec1e7 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -126,6 +126,7 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H organizationMember, err := database.ExpectOne(db.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: organization.ID, UserID: user.ID, + IncludeSystem: false, })) if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index 22e0edc3bc..54ec787661 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -10,6 +10,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/rbac" @@ -91,6 +92,7 @@ func (s AGPLIDPSync) SyncRoles(ctx context.Context, db database.Store, user data orgMemberships, err := tx.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: uuid.Nil, UserID: user.ID, + IncludeSystem: false, }) if err != nil { return xerrors.Errorf("get organizations by user id: %w", err) diff --git a/coderd/members.go b/coderd/members.go index 1852e64484..d1c4cdf017 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -160,6 +160,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { members, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: organization.ID, UserID: uuid.Nil, + IncludeSystem: false, }) if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) diff --git a/coderd/prebuilds/id.go b/coderd/prebuilds/id.go new file mode 100644 index 0000000000..7c2bbe79b7 --- /dev/null +++ b/coderd/prebuilds/id.go @@ -0,0 +1,5 @@ +package prebuilds + +import "github.com/google/uuid" + +var SystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0") diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 21e1c39fc0..144010c5bf 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -497,7 +497,7 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { return nil }) eg.Go(func() error { - groupMembers, err := r.options.Database.GetGroupMembers(ctx) + groupMembers, err := r.options.Database.GetGroupMembers(ctx, false) if err != nil { return xerrors.Errorf("get groups: %w", err) } diff --git a/coderd/userauth.go b/coderd/userauth.go index 63f54f6d15..9703eec43e 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -24,6 +24,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/jwtutils" @@ -1668,7 +1669,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C } // nolint:gocritic // Getting user count is a system function. - userCount, err := tx.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + userCount, err := tx.GetUserCount(dbauthz.AsSystemRestricted(ctx), false) if err != nil { return xerrors.Errorf("unable to fetch user count: %w", err) } diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index ee6ee957ba..4b67320164 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -28,6 +28,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" @@ -304,7 +305,7 @@ func TestUserOAuth2Github(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) // nolint:gocritic // Unit test - count, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + count, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx), false) require.NoError(t, err) require.Equal(t, int64(1), count) diff --git a/coderd/users.go b/coderd/users.go index 34969f3637..788c17df6d 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -85,7 +85,7 @@ func (api *API) userDebugOIDC(rw http.ResponseWriter, r *http.Request) { func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() // nolint:gocritic // Getting user count is a system function. - userCount, err := api.Database.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + userCount, err := api.Database.GetUserCount(dbauthz.AsSystemRestricted(ctx), false) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching user count.", @@ -128,7 +128,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { // This should only function for the first user. // nolint:gocritic // Getting user count is a system function. - userCount, err := api.Database.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + userCount, err := api.Database.GetUserCount(dbauthz.AsSystemRestricted(ctx), false) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching user count.", @@ -1192,6 +1192,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) { memberships, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{ UserID: user.ID, OrganizationID: uuid.Nil, + IncludeSystem: false, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ diff --git a/coderd/users_test.go b/coderd/users_test.go index cbd7607701..c21eca85a5 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -10,12 +10,13 @@ import ( "testing" "time" + "github.com/coder/serpent" + "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/coderd/rbac/policy" - "github.com/coder/serpent" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 778e9f9c2e..47f3b8757a 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -28,7 +28,7 @@ We track the following resources: | RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| | Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| -| User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | WorkspaceAgent
connect, disconnect | |
FieldTracked
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| | WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| | WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 6fd3f46308..84cc7d451b 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -151,6 +151,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "github_com_user_id": ActionIgnore, "hashed_one_time_passcode": ActionIgnore, "one_time_passcode_expires_at": ActionTrack, + "is_system": ActionTrack, // Should never change, but track it anyway. }, &database.WorkspaceTable{}: { "id": ActionTrack, diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 6b94adb2c5..3c5ecf6bfb 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -153,7 +153,10 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { return } - currentMembers, err := api.Database.GetGroupMembersByGroupID(ctx, group.ID) + currentMembers, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return @@ -170,6 +173,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { _, err := database.ExpectOne(api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{ OrganizationID: group.OrganizationID, UserID: uuid.MustParse(id), + IncludeSystem: false, })) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -282,7 +286,10 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { httpapi.InternalServerError(rw, err) } - patchedMembers, err := api.Database.GetGroupMembersByGroupID(ctx, group.ID) + patchedMembers, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return @@ -290,7 +297,10 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { aReq.New = group.Auditable(patchedMembers) - memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, group.ID) + memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, database.GetGroupMembersCountByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return @@ -333,7 +343,10 @@ func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) { return } - groupMembers, getMembersErr := api.Database.GetGroupMembersByGroupID(ctx, group.ID) + groupMembers, getMembersErr := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if getMembersErr != nil { httpapi.InternalServerError(rw, getMembersErr) return @@ -384,13 +397,19 @@ func (api *API) group(rw http.ResponseWriter, r *http.Request) { httpapi.InternalServerError(rw, err) } - users, err := api.Database.GetGroupMembersByGroupID(ctx, group.ID) + users, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.InternalServerError(rw, err) return } - memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, group.ID) + memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, database.GetGroupMembersCountByGroupIDParams{ + GroupID: group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return @@ -483,12 +502,18 @@ func (api *API) groups(rw http.ResponseWriter, r *http.Request) { resp := make([]codersdk.Group, 0, len(groups)) for _, group := range groups { - members, err := api.Database.GetGroupMembersByGroupID(ctx, group.Group.ID) + members, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return } - memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, group.Group.ID) + memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, database.GetGroupMembersCountByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 1baf62211d..690a476fcb 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -820,7 +820,6 @@ func TestGroup(t *testing.T) { t.Run("everyoneGroupReturnsEmpty", func(t *testing.T) { t.Parallel() - client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, @@ -829,8 +828,8 @@ func TestGroup(t *testing.T) { userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleUserAdmin()) _, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) - ctx := testutil.Context(t, testutil.WaitLong) + // The 'Everyone' group always has an ID that matches the organization ID. group, err := userAdminClient.Group(ctx, user.OrganizationID) require.NoError(t, err) diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index 6f0e827eb3..fbd53dcaac 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -33,7 +33,7 @@ func Entitlements( } // nolint:gocritic // Getting active user count is a system function. - activeUserCount, err := db.GetActiveUserCount(dbauthz.AsSystemRestricted(ctx)) + activeUserCount, err := db.GetActiveUserCount(dbauthz.AsSystemRestricted(ctx), false) // Don't include system user in license count. if err != nil { return codersdk.Entitlements{}, xerrors.Errorf("query active user count: %w", err) } diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index 37c0151749..b1f3d2cac3 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -62,14 +62,20 @@ func (api *API) templateAvailablePermissions(rw http.ResponseWriter, r *http.Req sdkGroups := make([]codersdk.Group, 0, len(groups)) for _, group := range groups { // nolint:gocritic - members, err := api.Database.GetGroupMembersByGroupID(dbauthz.AsSystemRestricted(ctx), group.Group.ID) + members, err := api.Database.GetGroupMembersByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return } // nolint:gocritic - memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), group.Group.ID) + memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersCountByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return @@ -138,13 +144,19 @@ func (api *API) templateACL(rw http.ResponseWriter, r *http.Request) { // them read the group members. // We should probably at least return more truncated user data here. // nolint:gocritic - members, err = api.Database.GetGroupMembersByGroupID(dbauthz.AsSystemRestricted(ctx), group.ID) + members, err = api.Database.GetGroupMembersByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return } // nolint:gocritic - memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), group.ID) + memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersCountByGroupIDParams{ + GroupID: group.Group.ID, + IncludeSystem: false, + }) if err != nil { httpapi.InternalServerError(rw, err) return diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index a40ed7b64a..b6c2048190 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -922,6 +922,7 @@ func TestTemplateACL(t *testing.T) { t.Run("everyoneGroup", func(t *testing.T) { t.Parallel() + client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, @@ -940,7 +941,7 @@ func TestTemplateACL(t *testing.T) { require.NoError(t, err) require.Len(t, acl.Groups, 1) - require.Len(t, acl.Groups[0].Members, 2) + require.Len(t, acl.Groups[0].Members, 2) // orgAdmin + TemplateAdmin require.Len(t, acl.Users, 0) }) diff --git a/enterprise/dbcrypt/cliutil.go b/enterprise/dbcrypt/cliutil.go index 120b41972d..a94760d3d6 100644 --- a/enterprise/dbcrypt/cliutil.go +++ b/enterprise/dbcrypt/cliutil.go @@ -7,6 +7,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" ) @@ -19,7 +20,7 @@ func Rotate(ctx context.Context, log slog.Logger, sqlDB *sql.DB, ciphers []Ciphe return xerrors.Errorf("create cryptdb: %w", err) } - userIDs, err := db.AllUserIDs(ctx) + userIDs, err := db.AllUserIDs(ctx, false) if err != nil { return xerrors.Errorf("get users: %w", err) } @@ -109,7 +110,7 @@ func Decrypt(ctx context.Context, log slog.Logger, sqlDB *sql.DB, ciphers []Ciph } cryptDB.primaryCipherDigest = "" - userIDs, err := db.AllUserIDs(ctx) + userIDs, err := db.AllUserIDs(ctx, false) if err != nil { return xerrors.Errorf("get users: %w", err) }