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) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy