From 51ca71c503fc9d7bde05a6eefc03f5ffd39046ec Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 30 Jun 2026 04:08:50 +0000 Subject: [PATCH 1/7] feat: database migration --- .../000534_all_service_accounts_all_the_time.down.sql | 2 ++ .../000534_all_service_accounts_all_the_time.up.sql | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql create mode 100644 coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql diff --git a/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql b/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql new file mode 100644 index 0000000000000..b668af387a28f --- /dev/null +++ b/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql @@ -0,0 +1,2 @@ +-- We do not track which users were converted to service accounts. +-- This is a destructive migration that cannot be undone. diff --git a/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql b/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql new file mode 100644 index 0000000000000..b3d5556adb973 --- /dev/null +++ b/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql @@ -0,0 +1,9 @@ +-- Convert legacy users created with login_type 'none' into service accounts. +-- Service accounts require empty email per users_email_not_empty. +UPDATE users +SET is_service_account = true, + email = '' +WHERE login_type = 'none' + AND is_service_account = false + -- `prebuilds@system` user should not convert it to service account. + AND is_system = false; From 8285fccc4c32f0c79ca0409a0b9ddf550575bf84 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 30 Jun 2026 04:22:45 +0000 Subject: [PATCH 2/7] feat: align cli and backend to disallow `login_type = none` --- cli/usercreate.go | 6 ++++++ coderd/userauth_test.go | 16 +++++++++++----- coderd/users.go | 7 +++++++ coderd/users_test.go | 11 +++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/cli/usercreate.go b/cli/usercreate.go index 1a904582593e2..24c8ea606a361 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -49,6 +49,12 @@ func (r *RootCmd) userCreate() *serpent.Command { if disableLogin && loginType != "" { return xerrors.New("You cannot specify both --disable-login and --login-type") } + if disableLogin && !serviceAccount { + return xerrors.New("--disable-login is deprecated. Use --service-account for machine-to-machine access.") + } + if loginType == string(codersdk.LoginTypeNone) && !serviceAccount { + return xerrors.New("Login type 'none' requires --service-account.") + } client, err := r.InitClient(inv) if err != nil { diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 8e4c230c4f308..7e7f748af43ec 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -153,13 +153,19 @@ func TestUserLogin(t *testing.T) { t.Run("LoginTypeNone", func(t *testing.T) { t.Parallel() - anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { - r.Password = "" - r.UserLoginType = codersdk.LoginTypeNone + client, db := coderdtest.NewWithDatabase(t, nil) + first := coderdtest.CreateFirstUser(t, client) + + noneUser := dbgen.User(t, db, database.User{ + LoginType: database.LoginTypeNone, + }) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: first.OrganizationID, + UserID: noneUser.ID, }) - _, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ - Email: anotherUser.Email, + _, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ + Email: noneUser.Email, Password: "SomeSecurePassword!", }) require.Error(t, err) diff --git a/coderd/users.go b/coderd/users.go index d067a637347f3..8725598598aed 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -488,6 +488,13 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { req.UserLoginType = codersdk.LoginTypePassword } + if !req.ServiceAccount && req.UserLoginType == codersdk.LoginTypeNone { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Login type 'none' requires a service account.", + }) + return + } + if req.UserLoginType != codersdk.LoginTypePassword && req.Password != "" { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Password cannot be set for non-password (%q) authentication.", req.UserLoginType), diff --git a/coderd/users_test.go b/coderd/users_test.go index 2893c7126b4a2..2a621e2772dc1 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -952,18 +952,17 @@ func TestPostUsers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{first.OrganizationID}, Email: "another@user.org", Username: "someone-else", Password: "", UserLoginType: codersdk.LoginTypeNone, }) - require.NoError(t, err) - - found, err := client.User(ctx, user.ID.String()) - require.NoError(t, err) - require.Equal(t, found.LoginType, codersdk.LoginTypeNone) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "service account") }) t.Run("CreateOIDCLoginType", func(t *testing.T) { From 519607e89d0e0c9e13e908d419797206702eecde Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 30 Jun 2026 04:31:16 +0000 Subject: [PATCH 3/7] fix: rename migration to `legacy_none_login_to_password.*` --- .../000534_all_service_accounts_all_the_time.down.sql | 2 -- .../000534_all_service_accounts_all_the_time.up.sql | 9 --------- .../000534_legacy_none_login_to_password.down.sql | 2 ++ .../000534_legacy_none_login_to_password.up.sql | 9 +++++++++ 4 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql delete mode 100644 coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql create mode 100644 coderd/database/migrations/000534_legacy_none_login_to_password.down.sql create mode 100644 coderd/database/migrations/000534_legacy_none_login_to_password.up.sql diff --git a/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql b/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql deleted file mode 100644 index b668af387a28f..0000000000000 --- a/coderd/database/migrations/000534_all_service_accounts_all_the_time.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- We do not track which users were converted to service accounts. --- This is a destructive migration that cannot be undone. diff --git a/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql b/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql deleted file mode 100644 index b3d5556adb973..0000000000000 --- a/coderd/database/migrations/000534_all_service_accounts_all_the_time.up.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Convert legacy users created with login_type 'none' into service accounts. --- Service accounts require empty email per users_email_not_empty. -UPDATE users -SET is_service_account = true, - email = '' -WHERE login_type = 'none' - AND is_service_account = false - -- `prebuilds@system` user should not convert it to service account. - AND is_system = false; diff --git a/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql b/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql new file mode 100644 index 0000000000000..b6ae9ef796235 --- /dev/null +++ b/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql @@ -0,0 +1,2 @@ +-- We do not track which users had login_type 'none' before this migration. +-- This is a destructive migration that cannot be undone. diff --git a/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql b/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql new file mode 100644 index 0000000000000..82c13214e7169 --- /dev/null +++ b/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql @@ -0,0 +1,9 @@ +-- Convert legacy users created with login_type 'none' to password auth. +-- OSS deployments cannot create service accounts without Premium. Existing +-- API tokens remain valid; admins can set a password if password login is +-- desired. +UPDATE users +SET login_type = 'password' +WHERE login_type = 'none' + AND is_service_account = false + AND is_system = false; From 07c468404842605e0c598f21dec74df5094c357e Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 13 Jul 2026 10:40:36 +1000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A4=96=20refactor(coderd):=20drop=20d?= =?UTF-8?q?estructive=20login=5Ftype=3Dnone=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grandfather existing login_type='none' users instead of converting them. The users_email_not_empty and users_service_account_login_type CHECK constraints make flipping real users to is_service_account=true infeasible without blanking emails, so leave existing accounts untouched and gate only at creation time. --- .../000534_legacy_none_login_to_password.up.sql | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 coderd/database/migrations/000534_legacy_none_login_to_password.up.sql diff --git a/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql b/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql deleted file mode 100644 index 82c13214e7169..0000000000000 --- a/coderd/database/migrations/000534_legacy_none_login_to_password.up.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Convert legacy users created with login_type 'none' to password auth. --- OSS deployments cannot create service accounts without Premium. Existing --- API tokens remain valid; admins can set a password if password login is --- desired. -UPDATE users -SET login_type = 'password' -WHERE login_type = 'none' - AND is_service_account = false - AND is_system = false; From d183df84bed2b157273f764f417f03a3101d5de0 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 13 Jul 2026 10:40:44 +1000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A4=96=20refactor(coderd):=20drop=20p?= =?UTF-8?q?aired=20down=20migration=20for=20login=5Ftype=3Dnone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/000534_legacy_none_login_to_password.down.sql | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 coderd/database/migrations/000534_legacy_none_login_to_password.down.sql diff --git a/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql b/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql deleted file mode 100644 index b6ae9ef796235..0000000000000 --- a/coderd/database/migrations/000534_legacy_none_login_to_password.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- We do not track which users had login_type 'none' before this migration. --- This is a destructive migration that cannot be undone. From 9cd17e058c12eb7797c6ada7212ab9e61f71344b Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 13 Jul 2026 10:46:40 +1000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A4=96=20feat(coderd):=20migrate=20le?= =?UTF-8?q?gacy=20login=5Ftype=3Dnone=20users=20to=20service=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert existing non-system login_type='none' users into service accounts (is_service_account=true). The users_email_not_empty CHECK constraint (migration 000433) requires service accounts to have an empty email, so this blanks the email of each converted user. This is a destructive, breaking, one-way migration: original emails are not recoverable. --- ...34_legacy_none_to_service_account.down.sql | 6 ++++++ ...0534_legacy_none_to_service_account.up.sql | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 coderd/database/migrations/000534_legacy_none_to_service_account.down.sql create mode 100644 coderd/database/migrations/000534_legacy_none_to_service_account.up.sql diff --git a/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql b/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql new file mode 100644 index 0000000000000..e82b1ce252cd0 --- /dev/null +++ b/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql @@ -0,0 +1,6 @@ +-- This migration is intentionally irreversible. +-- +-- The up migration converts legacy login_type='none' users into service +-- accounts and blanks their email to satisfy the users_email_not_empty +-- constraint. We do not record which users were affected, nor their original +-- email addresses, so there is nothing to restore here. diff --git a/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql b/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql new file mode 100644 index 0000000000000..a15587e711af5 --- /dev/null +++ b/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql @@ -0,0 +1,20 @@ +-- Convert legacy users created with login_type 'none' into service accounts. +-- +-- Historically, admins created machine-to-machine users with login_type +-- 'none'. That path is now deprecated in favour of premium service accounts, +-- so this migration grandfathers those existing accounts onto the service +-- account model (is_service_account = true). +-- +-- BREAKING / DESTRUCTIVE: the users_email_not_empty CHECK constraint (added in +-- migration 000433) requires that service accounts have an empty email. We +-- therefore MUST blank the email of every converted user. The original email +-- address is NOT recoverable. Email uniqueness indexes already exclude empty +-- emails (WHERE email != ''), so blanking multiple rows does not conflict. +-- +-- System users (is_system = true) are intentionally left untouched. +UPDATE users +SET is_service_account = true, + email = '' +WHERE login_type = 'none' + AND is_service_account = false + AND is_system = false; From 8b204dad93e579864a08db993995c06f8a4b50ce Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 13 Jul 2026 11:01:48 +1000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A4=96=20docs(coderd):=20tighten=20lo?= =?UTF-8?q?gin=5Ftype=3Dnone=20migration=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...34_legacy_none_to_service_account.down.sql | 7 +----- ...0534_legacy_none_to_service_account.up.sql | 24 ++++--------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql b/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql index e82b1ce252cd0..954c160ba2be7 100644 --- a/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql +++ b/coderd/database/migrations/000534_legacy_none_to_service_account.down.sql @@ -1,6 +1 @@ --- This migration is intentionally irreversible. --- --- The up migration converts legacy login_type='none' users into service --- accounts and blanks their email to satisfy the users_email_not_empty --- constraint. We do not record which users were affected, nor their original --- email addresses, so there is nothing to restore here. +-- Irreversible: original login_type and emails aren't recorded, nothing to restore. diff --git a/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql b/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql index a15587e711af5..42e7e73d93550 100644 --- a/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql +++ b/coderd/database/migrations/000534_legacy_none_to_service_account.up.sql @@ -1,20 +1,6 @@ --- Convert legacy users created with login_type 'none' into service accounts. --- --- Historically, admins created machine-to-machine users with login_type --- 'none'. That path is now deprecated in favour of premium service accounts, --- so this migration grandfathers those existing accounts onto the service --- account model (is_service_account = true). --- --- BREAKING / DESTRUCTIVE: the users_email_not_empty CHECK constraint (added in --- migration 000433) requires that service accounts have an empty email. We --- therefore MUST blank the email of every converted user. The original email --- address is NOT recoverable. Email uniqueness indexes already exclude empty --- emails (WHERE email != ''), so blanking multiple rows does not conflict. --- --- System users (is_system = true) are intentionally left untouched. +-- Convert legacy login_type='none' users into service accounts. +-- Service accounts must have an empty email (see 000433), so this blanks the +-- email of each converted user. Destructive and one-way. System users skipped. UPDATE users -SET is_service_account = true, - email = '' -WHERE login_type = 'none' - AND is_service_account = false - AND is_system = false; +SET is_service_account = true, email = '' +WHERE login_type = 'none' AND is_service_account = false AND is_system = false;