mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
chore: add prebuilds system user (#16916)
Pre-requisite for https://github.com/coder/coder/pull/16891 Closes https://github.com/coder/internal/issues/515 This PR introduces a new concept of a "system" user. Our data model requires that all workspaces have an owner (a `users` relation), and prebuilds is a feature that will spin up workspaces to be claimed later by actual users - and thus needs to own the workspaces in the interim. Naturally, introducing a change like this touches a few aspects around the codebase and we've taken the approach _default hidden_ here; in other words, queries for users will by default _exclude_ all system users, but there is a flag to ensure they can be displayed. This keeps the changeset relatively small. This user has minimal permissions (it's equivalent to a `member` since it has no roles). It will be associated with the default org in the initial migration, and thereafter we'll need to somehow ensure its membership aligns with templates (which are org-scoped) for which it'll need to provision prebuilds; that's a solution we'll have in a subsequent PR. --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
This commit is contained in:
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
4
coderd/database/dump.sql
generated
4
coderd/database/dump.sql
generated
@ -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
|
||||
|
@ -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',
|
||||
|
50
coderd/database/migrations/000308_system_user.down.sql
Normal file
50
coderd/database/migrations/000308_system_user.down.sql
Normal file
@ -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;
|
57
coderd/database/migrations/000308_system_user.up.sql
Normal file
57
coderd/database/migrations/000308_system_user.up.sql
Normal file
@ -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;
|
@ -423,6 +423,7 @@ func ConvertUserRows(rows []GetUsersRow) []User {
|
||||
AvatarURL: r.AvatarURL,
|
||||
Deleted: r.Deleted,
|
||||
LastSeenAt: r.LastSeenAt,
|
||||
IsSystem: r.IsSystem,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
5
coderd/prebuilds/id.go
Normal file
5
coderd/prebuilds/id.go
Normal file
@ -0,0 +1,5 @@
|
||||
package prebuilds
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
var SystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user