mirror of
https://github.com/coder/coder.git
synced 2026-06-06 06:28:20 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+7
-2
@@ -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 != '');
|
||||
+27
@@ -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
|
||||
);
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user