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)
}
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