fix!: deprecate login_type=none, convert existing users to password login (#26851)

> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Deprecates `login_type=none` (legacy passwordless machine users) in
favour of premium **service accounts**, and migrates existing accounts
off the deprecated path while preserving their identity. Resolves
[DEVEX-226].

## What this does

- **Creation is gated** — `POST /users` and `coder users create` reject
`login_type=none` (and the deprecated `--disable-login`) unless a
service account is requested.
- **Existing users are converted** — migration
`000554_legacy_none_login_to_password` rewrites legacy non-system,
non–service-account `login_type='none'` accounts to
`login_type='password'`. Email addresses are **preserved** and existing
API tokens remain valid. Admins can set a password if interactive login
is desired.

## Why convert to `password` and not `is_service_account`?

Migration `000433_add_is_service_account_to_users` adds two CHECK
constraints:

- `users_email_not_empty`: `(is_service_account = true) = (email = '')`
- `users_service_account_login_type`: `is_service_account = false OR
login_type = 'none'`

Turning a real, email-bearing `login_type=none` user into a service
account would require **blanking their email**. Converting to `password`
instead preserves the account and its email.

> ⚠️ **Breaking / one-way.** The `down` migration cannot restore which
users originally had `login_type='none'`.


Decision log

- **Goal:** move existing `login_type=none` users off the deprecated
path while preserving their identity/email.
- **Constraint discovered:** the `is_service_account` CHECK constraints
(migration `000433`) make a literal `none → service account` conversion
require blanking emails, so this PR converts to `password` instead to
keep emails intact.
- **Implementation:** creation-gating in `cli/usercreate.go` and
`coderd/users.go`, matching test updates, plus the
`000554_legacy_none_login_to_password.{up,down}.sql` migration.
- **CI fix:** the branch was behind `main` and its migration originally
numbered `000534`, which collided with main's
`000534_drop_chat_model_configs_provider`. Merged `main` and renumbered
to `000554` (next free after main's `000553`). `make gen` produces no
drift (the migration is data-only).



> The service-account conversion alternative (#27182, which blanked
emails) was closed in favour of this password-preserving approach.
>
> Docs follow-up: #27333.

[DEVEX-226]: https://linear.app/issue/DEVEX-226

---------

Co-authored-by: Sushant P <zenithwolf1000@users.noreply.github.com>
This commit is contained in:
Jake Howell
2026-07-28 21:05:50 +10:00
committed by GitHub
co-authored by Sushant P
parent ed37483ff7
commit 0e104f38e0
8 changed files with 198 additions and 16 deletions
+10 -5
View File
@@ -44,10 +44,15 @@ func (r *RootCmd) userCreate() *serpent.Command {
case disableLogin:
return xerrors.New("You cannot use --disable-login with --service-account")
}
}
if disableLogin && loginType != "" {
return xerrors.New("You cannot specify both --disable-login and --login-type")
} else {
switch {
case disableLogin && loginType != "":
return xerrors.New("You cannot specify both --disable-login and --login-type")
case disableLogin:
return xerrors.New("--disable-login is deprecated. Use --service-account for machine-to-machine access.")
case loginType == string(codersdk.LoginTypeNone):
return xerrors.New("Login type 'none' is deprecated. Use --service-account for machine-to-machine access.")
}
}
client, err := r.InitClient(inv)
@@ -200,7 +205,7 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
{
Flag: "disable-login",
Hidden: true,
Description: "Deprecated: Use '--login-type=none'. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
Description: "Deprecated: Use --service-account (requires Premium) for machine-to-machine access. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
"Be careful when using this flag as it can lock the user out of their account.",
Value: serpent.BoolOf(&disableLogin),
},
+15
View File
@@ -160,6 +160,21 @@ func TestUserCreate(t *testing.T) {
args: []string{"--service-account", "-u", "dean", "--password", "1n5ecureP4ssw0rd!"},
err: "You cannot use --password with --service-account",
},
{
name: "DisableLogin",
args: []string{"--disable-login", "-u", "dean"},
err: "--disable-login is deprecated. Use --service-account for machine-to-machine access.",
},
{
name: "LoginTypeNone",
args: []string{"--login-type", "none", "-u", "dean"},
err: "Login type 'none' is deprecated. Use --service-account for machine-to-machine access.",
},
{
name: "DisableLoginWithLoginType",
args: []string{"--disable-login", "--login-type", "password", "-u", "dean"},
err: "You cannot specify both --disable-login and --login-type",
},
}
for _, tt := range tests {