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
|
Field | Tracked |
| field | true |
mapping | true |
|
| Template
write, delete | Field | Tracked |
| active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_display_name | false |
organization_icon | false |
organization_id | false |
organization_name | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
| archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
source_example_id | false |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
| avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | false |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
+| User
create, write, delete | Field | Tracked |
| avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | false |
hashed_password | true |
id | true |
is_system | true |
last_seen_at | false |
login_type | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
| WorkspaceAgent
connect, disconnect | Field | Tracked |
| api_version | false |
architecture | false |
auth_instance_id | false |
auth_token | false |
connection_timeout_seconds | false |
created_at | false |
directory | false |
disconnected_at | false |
display_apps | false |
display_order | false |
environment_variables | false |
expanded_directory | false |
first_connected_at | false |
id | false |
instance_metadata | false |
last_connected_at | false |
last_connected_replica_id | false |
lifecycle_state | false |
logs_length | false |
logs_overflowed | false |
motd_file | false |
name | false |
operating_system | false |
ready_at | false |
resource_id | false |
resource_metadata | false |
started_at | false |
subsystems | false |
troubleshooting_url | false |
updated_at | false |
version | false |
|
| WorkspaceApp
open, close | Field | Tracked |
| agent_id | false |
command | false |
created_at | false |
display_name | false |
display_order | false |
external | false |
health | false |
healthcheck_interval | false |
healthcheck_threshold | false |
healthcheck_url | false |
hidden | false |
icon | false |
id | false |
open_in | false |
sharing_level | false |
slug | false |
subdomain | false |
url | false |
|
| WorkspaceBuild
start, stop | Field | Tracked |
| build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
template_version_preset_id | false |
transition | false |
updated_at | false |
workspace_id | false |
|
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)
}