diff --git a/cli/server.go b/cli/server.go index 0b64cd8aa6899..3fefc51357d0d 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 94c0c7ef62c56..275ca1fc3ca75 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 149051bd3bc64..b280fa890244f 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 04d816629ac65..a9f26e303d644 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 eec6e90d5904a..de45f90d91f2a 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 56e272c7ba048..2596d843eaa0c 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 4d19aa65298a2..3eb40842e693e 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 338945556284b..ac824c9fff2a8 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 f36a7aeaf357a..e1320cf88fb0d 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 04333c0ed2ad4..225a1107122b6 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 0000000000000..69903b13d3cc5 --- /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 0000000000000..c024a9587f774 --- /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 a9dbc3e530994..5b197a0649dcf 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 c8c6ec2d968ec..3c437cde293d3 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 0ff030271d38b..201a57a4d6b94 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 0c4928e7ffb30..2dc5f4016f2fc 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 837068f1fa03e..a2d22f9144fb6 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 17ab7ef3e3fe7..6f5e5813c1a75 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 4efe9bf488590..7de8dbe4e4523 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 a92cd681eabf6..9d570bc1c49ee 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 0c29cf723f7ef..c4304cfc3e60e 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 2eba0dcedf5b8..18938ec1e792d 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 22e0edc3bc662..54ec787661826 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 1852e6448408f..d1c4cdf01770c 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 0000000000000..7c2bbe79b7a6f --- /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 21e1c39fc096f..144010c5bf122 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 63f54f6d157ff..9703eec43e6e5 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 ee6ee957ba861..4b67320164fc2 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 34969f363737c..788c17df6d9cd 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 cbd7607701c1f..c21eca85a5ee7 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 778e9f9c2e26e..47f3b8757a7bb 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 6fd3f46308975..84cc7d451b4f1 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 6b94adb2c5b78..3c5ecf6bfbff5 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 1baf62211dcd9..690a476fcb1ba 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 6f0e827eb3320..fbd53dcaac58c 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 37c0151749196..b1f3d2cac3ac5 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 a40ed7b64a6db..b6c2048190e9a 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 120b41972de05..a94760d3d6e65 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) }