You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(coderd_user): add service account support (#357)
Adds an `is_service_account` attribute to the `coderd_user` resource and
data source, closing the Terraform/API parity gap for service accounts.
Service accounts are admin-managed, login-less accounts that — unlike a
`login_type = "none"` user — carry no email and do not consume a
licensed user seat.
Changes:
- **Resource**: add `is_service_account` (optional, defaults `false`,
`ForceNew` since it is immutable server-side). When `true`, the user is
created via `CreateUserWithOrgs` with `ServiceAccount: true` (no email,
`login_type` `none`); the existing `CreateUser` path is unchanged for
regular users.
- **Plan-time validation** (`ValidateConfig`) mirrors the server rules:
`email` is required unless `is_service_account` is `true`, in which case
`email`, `password`, and a non-`none` `login_type` are rejected.
- **Data source**: expose `is_service_account` (read-only).
- `email` becomes `Optional + Computed` (was `Required`) so it can be
omitted for service accounts; it remains effectively required for
regular users via `ValidateConfig`.
- Add an acceptance test (`TestAccUserResourceServiceAccount`, gated on
a licensed deployment via `UseLicense` since service accounts are
Premium) and regenerate docs/examples.
The attribute name matches the API's `is_service_account` field,
consistent with how `is_default` is named on the `coderd_organization`
data source; on create it maps to the `service_account` field of
`CreateUserRequestWithOrgs` (the create request uses the unprefixed
name), the same attribute-name-to-request-field mapping already used for
`suspended`.
Verified locally: `go build ./...`, `golangci-lint run` (clean), `make
gen` (idempotent). Acceptance tests require a licensed deployment and
run in CI.
Fixes#356
---------
Co-authored-by: PushTheLimit <591079+PushTheLimit@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ethan Dickson <ethanndickson@gmail.com>
-`created_at` (Number) Unix timestamp of when the user was created.
49
49
-`email` (String) Email of the user.
50
+
-`is_service_account` (Boolean) Whether the user is a service account: an admin-managed account that cannot log in interactively and does not consume a licensed user seat.
50
51
-`last_seen_at` (Number) Unix timestamp of when the user was last seen.
51
52
-`login_type` (String) Type of login for the user. Valid types are `none`, `password', `github`, and `oidc`.
// Create a service account for automation (Premium). Unlike a `login_type =
46
+
// none` user, a service account has no email and does not consume a user seat.
47
+
resource "coderd_user" "automation" {
48
+
username = "automation"
49
+
name = "Automation"
50
+
roles = ["template-admin"]
51
+
is_service_account = true
52
+
}
44
53
```
45
54
46
55
<!-- schema generated by tfplugindocs -->
47
56
## Schema
48
57
49
58
### Required
50
59
51
-
-`email` (String) Email address of the user.
52
60
-`username` (String) Username of the user.
53
61
54
62
### Optional
55
63
64
+
-`email` (String) Email address of the user. Required unless `is_service_account` is `true`, in which case it must be omitted (service accounts have no email).
65
+
-`is_service_account` (Boolean) Whether the user is a service account. Service accounts are admin-managed accounts that cannot log in interactively: they have no password or email and use `login_type``none`. Unlike a regular `login_type = none` user, a service account does not consume a licensed user seat. Changing this attribute forces replacement.
56
66
-`login_type` (String) Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.
57
67
-`name` (String) Display name of the user. Defaults to username.
58
68
-`password` (String, Sensitive) Password for the user. Required when `login_type` is `password`. Passwords are saved into the state as plain text and should only be used for testing purposes.
MarkdownDescription: "Whether the user is suspended.",
84
85
Computed: true,
85
86
},
87
+
"is_service_account": schema.BoolAttribute{
88
+
MarkdownDescription: "Whether the user is a service account: an admin-managed account that cannot log in interactively and does not consume a licensed user seat.",
MarkdownDescription: "Email address of the user.",
87
-
Required: true,
89
+
MarkdownDescription: "Email address of the user. Required unless `is_service_account` is `true`, in which case it must be omitted (service accounts have no email).",
90
+
Optional: true,
91
+
Computed: true,
92
+
PlanModifiers: []planmodifier.String{
93
+
stringplanmodifier.UseStateForUnknown(),
94
+
stringplanmodifier.RequiresReplaceIfConfigured(),
95
+
},
88
96
},
89
97
"roles": schema.SetAttribute{
90
98
MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`. If `null`, roles will not be managed by Terraform. This attribute must be null if the user is an OIDC user and role sync is configured",
MarkdownDescription: "Whether the user is a service account. Service accounts are admin-managed accounts that cannot log in interactively: they have no password or email and use `login_type` `none`. Unlike a regular `login_type = none` user, a service account does not consume a licensed user seat. Changing this attribute forces replacement.",
0 commit comments