feat: backend support for creating and storing service accounts (#22698)

Add is_service_account column to users table with CHECK constraints
enforcing login_type='none' and empty email for service accounts.
Update user creation API to validate service account constraints.

Related to:
https://linear.app/codercom/issue/PLAT-27/feat-backend-support-for-creating-and-storing-service-accounts
This commit is contained in:
George K
2026-03-11 10:19:08 -07:00
committed by GitHub
parent e96cd5cbb2
commit e5c19d0af4
28 changed files with 522 additions and 87 deletions
+2
View File
@@ -12,6 +12,8 @@ const (
CheckChatProvidersProviderCheck CheckConstraint = "chat_providers_provider_check" // chat_providers
CheckOrganizationIDNotZero CheckConstraint = "organization_id_not_zero" // custom_roles
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
CheckUsersEmailNotEmpty CheckConstraint = "users_email_not_empty" // users
CheckUsersServiceAccountLoginType CheckConstraint = "users_service_account_login_type" // users
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
+20 -10
View File
@@ -578,17 +578,27 @@ func WorkspaceBuildParameters(t testing.TB, db database.Store, orig []database.W
}
func User(t testing.TB, db database.Store, orig database.User) database.User {
loginType := takeFirst(orig.LoginType, database.LoginTypePassword)
email := takeFirst(orig.Email, testutil.GetRandomName(t))
// A DB constraint requires login_type = 'none' and email = '' for service
// accounts.
if orig.IsServiceAccount {
loginType = database.LoginTypeNone
email = ""
}
user, err := db.InsertUser(genCtx, database.InsertUserParams{
ID: takeFirst(orig.ID, uuid.New()),
Email: takeFirst(orig.Email, testutil.GetRandomName(t)),
Username: takeFirst(orig.Username, testutil.GetRandomName(t)),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
HashedPassword: takeFirstSlice(orig.HashedPassword, []byte(must(cryptorand.String(32)))),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}),
LoginType: takeFirst(orig.LoginType, database.LoginTypePassword),
Status: string(takeFirst(orig.Status, database.UserStatusDormant)),
ID: takeFirst(orig.ID, uuid.New()),
Email: email,
Username: takeFirst(orig.Username, testutil.GetRandomName(t)),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
HashedPassword: takeFirstSlice(orig.HashedPassword, []byte(must(cryptorand.String(32)))),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}),
LoginType: loginType,
Status: string(takeFirst(orig.Status, database.UserStatusDormant)),
IsServiceAccount: orig.IsServiceAccount,
})
require.NoError(t, err, "insert user")
+14
View File
@@ -213,6 +213,20 @@ func TestGenerator(t *testing.T) {
require.Equal(t, exp, must(db.GetUserByID(context.Background(), exp.ID)))
})
t.Run("ServiceAccountUser", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
user := dbgen.User(t, db, database.User{
IsServiceAccount: true,
Email: "should-be-overridden@coder.com",
LoginType: database.LoginTypePassword,
})
require.True(t, user.IsServiceAccount)
require.Empty(t, user.Email)
require.Equal(t, database.LoginTypeNone, user.LoginType)
require.Equal(t, user, must(db.GetUserByID(context.Background(), user.ID)))
})
t.Run("SSHKey", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
+7 -2
View File
@@ -1471,7 +1471,10 @@ CREATE TABLE users (
hashed_one_time_passcode bytea,
one_time_passcode_expires_at timestamp with time zone,
is_system boolean DEFAULT false NOT NULL,
is_service_account 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)))),
CONSTRAINT users_email_not_empty CHECK (((is_service_account = true) = (email = ''::text))),
CONSTRAINT users_service_account_login_type CHECK (((is_service_account = false) OR (login_type = 'none'::login_type))),
CONSTRAINT users_username_min_length CHECK ((length(username) >= 1))
);
@@ -1487,6 +1490,8 @@ COMMENT ON COLUMN users.one_time_passcode_expires_at IS 'The time when the one-t
COMMENT ON COLUMN users.is_system IS 'Determines if a user is a system user, and therefore cannot login or perform normal actions';
COMMENT ON COLUMN users.is_service_account IS 'Determines if a user is an admin-managed account that cannot login';
CREATE VIEW group_members_expanded AS
WITH all_members AS (
SELECT group_members.user_id,
@@ -3601,7 +3606,7 @@ CREATE INDEX idx_user_deleted_deleted_at ON user_deleted USING btree (deleted_at
CREATE INDEX idx_user_status_changes_changed_at ON user_status_changes USING btree (changed_at);
CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE ((deleted = false) AND (email <> ''::text));
CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false);
@@ -3651,7 +3656,7 @@ CREATE UNIQUE INDEX user_secrets_user_file_path_idx ON user_secrets USING btree
CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
@@ -4,10 +4,12 @@ ALTER TABLE users ADD COLUMN IF NOT EXISTS
-- Copy "theme_preference" back to "users"
UPDATE users
SET theme_preference = (SELECT value
FROM user_configs
WHERE user_configs.user_id = users.id
AND user_configs.key = 'theme_preference');
-- Use COALESCE(SELECT, <default>) to avoid forcing an insert of user_configs
-- for every users insert in order for this down migration to succeed.
SET theme_preference = COALESCE(
(SELECT value FROM user_configs WHERE user_configs.user_id = users.id AND user_configs.key = 'theme_preference'),
''
);
-- Drop the "user_configs" table.
DROP TABLE user_configs;
@@ -0,0 +1,18 @@
-- Since we can't simply delete a user that potentially has all kinds of tables
-- referencing it, give service accounts with empty emails a unique placeholder
-- so the original unique indexes can be restored. We only run down migrations
-- in dev, so hopefully this is not a big deal.
UPDATE users SET
email = 'ex-service-account-' || id::text || '@localhost',
is_service_account = false
WHERE is_service_account = true AND email = '';
-- Restore original unique indexes.
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX IF EXISTS users_email_lower_idx;
CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_not_empty;
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_service_account_login_type;
ALTER TABLE users DROP COLUMN is_service_account;
@@ -0,0 +1,23 @@
ALTER TABLE users ADD COLUMN is_service_account boolean NOT NULL DEFAULT false;
COMMENT ON COLUMN users.is_service_account IS 'Determines if a user is an admin-managed account that cannot login';
-- Service accounts must use login_type 'none'.
ALTER TABLE users ADD CONSTRAINT users_service_account_login_type CHECK (is_service_account = false OR login_type = 'none');
-- Paranoia check: mark any (unlikely) existing user with an empty email as a
-- service account so that adding the constraint below does not fail.
-- NOTE: considered setting email to nobody@localhost instead but for all we
-- know it may already exist, so chose the lesser of two evils.
UPDATE users SET is_service_account = true, login_type = 'none' WHERE email = '';
-- Service accounts must have empty email; other users must not.
ALTER TABLE users ADD CONSTRAINT users_email_not_empty CHECK ((is_service_account = true) = (email = ''));
-- Exclude empty emails from uniqueness so multiple service accounts can omit an
-- email without conflicting. This is the less invasive alternative to making
-- email nullable, which would require a big refactor.
DROP INDEX idx_users_email;
DROP INDEX users_email_lower_idx;
CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false AND email != '');
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false AND email != '');
@@ -0,0 +1,27 @@
-- Fixture for migration 000433_add_is_service_account_to_users.
-- Inserts a user with an empty email to ensure the migration
-- correctly marks them as a service account before adding the
-- users_email_not_empty constraint.
INSERT INTO users (
id,
email,
username,
hashed_password,
created_at,
updated_at,
status,
rbac_roles,
login_type
)
VALUES (
'8ddb584a-68b8-48ac-998f-86f091ccb380',
'',
'fixture-empty-email-user-to-service-account',
'',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00',
'active',
'{}',
'password'
);
@@ -0,0 +1,41 @@
-- Fixture for migration 000433_add_is_service_account_to_users.
-- Inserts multiple service accounts with empty emails to help test
-- the down migration, which must assign each a unique placeholder
-- email before restoring the original unique index on email.
INSERT INTO users (
id,
email,
username,
hashed_password,
created_at,
updated_at,
status,
rbac_roles,
login_type,
is_service_account
)
VALUES (
'b2ce097d-2287-4d64-a550-ed821969545d',
'',
'fixture-service-account-1',
'',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00',
'active',
'{}',
'none',
true
),
(
'3e218a4a-3b4a-4242-b24e-9430277e619d',
'',
'fixture-service-account-2',
'',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00',
'active',
'{}',
'none',
true
);
+15 -14
View File
@@ -663,20 +663,21 @@ func ConvertUserRows(rows []GetUsersRow) []User {
users := make([]User, len(rows))
for i, r := range rows {
users[i] = User{
ID: r.ID,
Email: r.Email,
Username: r.Username,
Name: r.Name,
HashedPassword: r.HashedPassword,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
Status: r.Status,
RBACRoles: r.RBACRoles,
LoginType: r.LoginType,
AvatarURL: r.AvatarURL,
Deleted: r.Deleted,
LastSeenAt: r.LastSeenAt,
IsSystem: r.IsSystem,
ID: r.ID,
Email: r.Email,
Username: r.Username,
Name: r.Name,
HashedPassword: r.HashedPassword,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
Status: r.Status,
RBACRoles: r.RBACRoles,
LoginType: r.LoginType,
AvatarURL: r.AvatarURL,
Deleted: r.Deleted,
LastSeenAt: r.LastSeenAt,
IsSystem: r.IsSystem,
IsServiceAccount: r.IsServiceAccount,
}
}
+1
View File
@@ -480,6 +480,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.Count,
); err != nil {
return nil, err
+2
View File
@@ -4877,6 +4877,8 @@ type User struct {
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"`
// Determines if a user is an admin-managed account that cannot login
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
}
type UserConfig struct {
+78
View File
@@ -1854,6 +1854,84 @@ func TestUpdateSystemUser(t *testing.T) {
require.NoError(t, err)
}
func TestInsertUserServiceAccountConstraints(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
// Happy path: should succeed.
t.Run("ServiceAccountWithEmptyEmailAndLoginNone", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
user, err := db.InsertUser(ctx, database.InsertUserParams{
Email: "",
LoginType: database.LoginTypeNone,
ID: uuid.New(),
Username: "sa-ok",
RBACRoles: []string{},
IsServiceAccount: true,
})
require.NoError(t, err)
require.True(t, user.IsServiceAccount)
require.Empty(t, user.Email)
})
// Service account with a non-empty email should be rejected
// by the users_email_not_empty constraint.
t.Run("ServiceAccountWithNonEmptyEmail", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
_, err := db.InsertUser(ctx, database.InsertUserParams{
Email: "sa@coder.com",
LoginType: database.LoginTypeNone,
ID: uuid.New(),
Username: "sa-with-email",
RBACRoles: []string{},
IsServiceAccount: true,
})
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckUsersEmailNotEmpty))
})
// A non-service-account with empty email should be rejected
// by the users_email_not_empty constraint.
t.Run("RegularUserWithEmptyEmail", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
_, err := db.InsertUser(ctx, database.InsertUserParams{
Email: "",
LoginType: database.LoginTypePassword,
ID: uuid.New(),
Username: "regular-no-email",
RBACRoles: []string{},
IsServiceAccount: false,
})
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckUsersEmailNotEmpty))
})
// Service account with login_type!=none should be rejected
// by the users_service_account_login_type constraint.
t.Run("ServiceAccountWithPasswordLoginType", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
_, err := db.InsertUser(ctx, database.InsertUserParams{
Email: "",
LoginType: database.LoginTypePassword,
ID: uuid.New(),
Username: "sa-with-password",
RBACRoles: []string{},
IsServiceAccount: true,
})
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckUsersServiceAccountLoginType))
})
}
func TestUserChangeLoginType(t *testing.T) {
t.Parallel()
if testing.Short() {
+42 -26
View File
@@ -18822,19 +18822,19 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(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, is_system
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, is_service_account
FROM
users
WHERE
(LOWER(username) = LOWER($1) OR LOWER(email) = LOWER($2)) AND
(LOWER(username) = LOWER($1) OR ($2 != '' AND LOWER(email) = LOWER($2))) AND
deleted = false
LIMIT
1
`
type GetUserByEmailOrUsernameParams struct {
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
Email interface{} `db:"email" json:"email"`
}
func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) {
@@ -18859,13 +18859,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
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, is_system
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, is_service_account
FROM
users
WHERE
@@ -18896,6 +18897,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -18987,7 +18989,7 @@ func (q *sqlQuerier) GetUserThemePreference(ctx context.Context, userID uuid.UUI
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, is_system, 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, is_service_account, COUNT(*) OVER() AS count
FROM
users
WHERE
@@ -19128,6 +19130,7 @@ type GetUsersRow struct {
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"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
Count int64 `db:"count" json:"count"`
}
@@ -19175,6 +19178,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.Count,
); err != nil {
return nil, err
@@ -19191,7 +19195,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, is_system 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, is_service_account FROM users WHERE id = ANY($1 :: uuid [ ])
`
// This shouldn't check for deleted, because it's frequently used
@@ -19225,6 +19229,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
); err != nil {
return nil, err
}
@@ -19251,27 +19256,30 @@ INSERT INTO
updated_at,
rbac_roles,
login_type,
status
status,
is_service_account
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9,
-- 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, is_system
COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status),
$11::bool
) 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, is_service_account
`
type InsertUserParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
Status string `db:"status" json:"status"`
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
Status string `db:"status" json:"status"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
}
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
@@ -19286,6 +19294,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
arg.RBACRoles,
arg.LoginType,
arg.Status,
arg.IsServiceAccount,
)
var i User
err := row.Scan(
@@ -19307,6 +19316,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19473,7 +19483,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, is_system
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, is_service_account
`
type UpdateUserLastSeenAtParams struct {
@@ -19504,6 +19514,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19523,7 +19534,7 @@ SET
WHERE
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
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, is_service_account
`
type UpdateUserLoginTypeParams struct {
@@ -19553,6 +19564,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19568,7 +19580,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, 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, is_service_account
`
type UpdateUserProfileParams struct {
@@ -19609,6 +19621,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19620,7 +19633,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, 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, is_service_account
`
type UpdateUserQuietHoursScheduleParams struct {
@@ -19650,6 +19663,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19662,7 +19676,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, 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, is_service_account
`
type UpdateUserRolesParams struct {
@@ -19692,6 +19706,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
@@ -19705,7 +19720,7 @@ SET
-- If the user is logging in, set last_seen_at to updated_at.
last_seen_at = CASE WHEN $4 :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
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, is_system
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, is_service_account
`
type UpdateUserStatusParams struct {
@@ -19742,6 +19757,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
)
return i, err
}
+5 -3
View File
@@ -57,7 +57,7 @@ SELECT
FROM
users
WHERE
(LOWER(username) = LOWER(@username) OR LOWER(email) = LOWER(@email)) AND
(LOWER(username) = LOWER(@username) OR (@email != '' AND LOWER(email) = LOWER(@email))) AND
deleted = false
LIMIT
1;
@@ -92,13 +92,15 @@ INSERT INTO
updated_at,
rbac_roles,
login_type,
status
status,
is_service_account
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9,
-- if the status passed in is empty, fallback to dormant, which is what
-- we were doing before.
COALESCE(NULLIF(@status::text, '')::user_status, 'dormant'::user_status)
COALESCE(NULLIF(@status::text, '')::user_status, 'dormant'::user_status),
@is_service_account::bool
) RETURNING *;
-- name: UpdateUserProfile :one
+2 -2
View File
@@ -125,7 +125,7 @@ const (
UniqueIndexProvisionerDaemonsOrgNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_org_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_org_name_owner_key ON provisioner_daemons USING btree (organization_id, name, lower(COALESCE((tags ->> 'owner'::text), ''::text)));
UniqueIndexTemplateVersionPresetsDefault UniqueConstraint = "idx_template_version_presets_default" // CREATE UNIQUE INDEX idx_template_version_presets_default ON template_version_presets USING btree (template_version_id) WHERE (is_default = true);
UniqueIndexUniquePresetName UniqueConstraint = "idx_unique_preset_name" // CREATE UNIQUE INDEX idx_unique_preset_name ON template_version_presets USING btree (name, template_version_id);
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE ((deleted = false) AND (email <> ''::text));
UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false);
UniqueNotificationMessagesDedupeHashIndex UniqueConstraint = "notification_messages_dedupe_hash_idx" // CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash);
UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true);
@@ -137,7 +137,7 @@ const (
UniqueUserSecretsUserEnvNameIndex UniqueConstraint = "user_secrets_user_env_name_idx" // CREATE UNIQUE INDEX user_secrets_user_env_name_idx ON user_secrets USING btree (user_id, env_name) WHERE (env_name <> ''::text);
UniqueUserSecretsUserFilePathIndex UniqueConstraint = "user_secrets_user_file_path_idx" // CREATE UNIQUE INDEX user_secrets_user_file_path_idx ON user_secrets USING btree (user_id, file_path) WHERE (file_path <> ''::text);
UniqueUserSecretsUserNameIndex UniqueConstraint = "user_secrets_user_name_idx" // CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
UniqueWorkspaceAppAuditSessionsUniqueIndex UniqueConstraint = "workspace_app_audit_sessions_unique_index" // CREATE UNIQUE INDEX workspace_app_audit_sessions_unique_index ON workspace_app_audit_sessions USING btree (agent_id, app_id, user_id, ip, user_agent, slug_or_port, status_code);
UniqueWorkspaceProxiesLowerNameIndex UniqueConstraint = "workspace_proxies_lower_name_idx" // CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);