Terraform module which manages a Temporal Cloud user: their account-level role, any custom roles, and their per-namespace permissions.
terraform apply on this module sends an invitation email. That is not a metaphor for creating a
record: Temporal Cloud mails the address in email a link, and
the person has to click it and sign up before they can sign in.
Three consequences that make this module behave unlike most Terraform resources:
- Apply has a side effect outside your infrastructure. Someone receives mail. A misspelled address
invites a stranger, or nobody. Re-running
applywill not un-send it. terraform destroyrevokes a real person's access. Removing amoduleblock or a key from a wrapper'sitemsmap takes away that person's ability to sign in to the account. Read the plan.- Acceptance happens outside Terraform.
user_stateis the provisioning state of the user record — the generic Temporal Cloud resource lifecycle, the same enum namespaces and groups use. It is not documented as a signal of whether the invitation was accepted, so do not treat it as one. Check the Temporal Cloud UI ortcld userfor that.
Changing email on an existing user replaces it: the previous address loses access and a fresh
invitation goes to the new one. Renaming a person's address is a destroy-and-invite, not an update.
If your account has SCIM, this is probably not the module you want for day-to-day access. SCIM provisions users and their group memberships from your identity provider, so managing the same people here means two systems writing the same records.
What SCIM does not do is assign permissions. Temporal Cloud's SCIM documentation is explicit that roles are configured separately after a group syncs. So the division that works is:
| Concern | Owned by |
|---|---|
| Authentication — signing in | SAML SSO |
| Which people exist, and their group membership | SCIM, from your identity provider |
| What a group is allowed to do | the group module |
| Machine access for workers and CI | the service-account module — never SCIM |
With SCIM in place, grant access to groups rather than to people: point the group module at a
SCIM-provisioned group with create_group = false and manage its account_access and
namespace_accesses. Individuals then inherit permissions from membership, and an account role of
none on a user is valid precisely to support that pattern.
Use this module when:
- Your account has no SCIM. SAML and SCIM are both paid features, so plenty of accounts manage users directly — in which case this is the only way to keep them in version control.
- Break-glass access. An administrator deliberately outside the corporate identity provider, so an SSO outage does not lock you out of the control plane.
- People who are not in the directory — a contractor, a partner, a vendor.
- A one-off exception. SCIM maps groups to roles. One person needing read on one namespace, without inventing a directory group for them, is a per-user grant.
Do not use this module for workers, CI, or any automated client. Those authenticate with an API key issued to a service account, which is a different resource and never a user.
The temporalcloud provider authenticates with an API key, read from the TEMPORAL_CLOUD_API_KEY
environment variable:
export TEMPORAL_CLOUD_API_KEY="<your-api-key>"The provider authenticates when it initialises, so a key is needed even for a terraform plan that
would create nothing. Keep the key out of version control — an untracked .env file rather than a
committed .tfvars.
Each user counts towards the account's user limit — 300 by default — from the moment they are invited until the resource is destroyed.
module "user" {
source = "terraform-temporalcloud-modules/user/temporalcloud"
version = "~> 2.0"
email = "ana@example.com"
account_access = "developer"
# This set is the user's COMPLETE namespace access map. Removing an entry
# revokes that access on the next apply.
namespace_accesses = [
{
namespace_id = module.orders_namespace.namespace_id
permission = "write"
},
{
namespace_id = module.payments_namespace.namespace_id
permission = "read"
},
]
}module "auditor" {
source = "terraform-temporalcloud-modules/user/temporalcloud"
version = "~> 2.0"
email = "auditor@example.com"
account_access = "read"
namespace_accesses = [
{
namespace_id = module.orders_namespace.namespace_id
permission = "read"
},
]
}Admins reach every namespace implicitly, so they take no namespace_accesses at all:
module "platform_admin" {
source = "terraform-temporalcloud-modules/user/temporalcloud"
version = "~> 2.0"
email = "platform@example.com"
account_access = "admin"
}Custom roles stack on top of the built-in account_access role rather than replacing it. A single
principal can hold at most 10:
module "finance_viewer" {
source = "terraform-temporalcloud-modules/user/temporalcloud"
version = "~> 2.0"
email = "finance@example.com"
account_access = "read"
account_access_custom_roles = [temporalcloud_custom_role.billing_reader.id]
}account_access is the user's account-wide role. It is required, and it interacts with
namespace_accesses in ways worth knowing before you plan:
| Role | Namespace grants | Notes |
|---|---|---|
owner |
not allowed | Import only. Cannot be created, changed or removed without Temporal support |
admin |
not allowed | Reaches every namespace implicitly |
developer |
expected | The usual choice for someone who needs specific namespaces |
read |
expected | Account-wide read; still needs grants to see workflows in a namespace |
financeadmin |
expected | Billing administration |
metricsread |
expected | Metrics endpoint access |
Values are matched case-insensitively. These six are the whole set the provider accepts. A
SCIM-managed user whose role comes from group membership reads back as none, but none cannot be
set — the provider rejects it in configuration, so this module does not accept it either.
The vocabulary is not shared with groups: temporalcloud_group_access accepts none and rejects
financeadmin and metricsread, which is the mirror image of the list above.
Combining admin or owner with namespace_accesses is refused during plan:
Error: Resource precondition failed
namespace_accesses cannot be combined with an account_access of owner or admin:
those roles already have access to every namespace.
The provider enforces the same rule itself (Users with account_access roles of owner or admin cannot have namespace accesses), so neither reaches the API. The module's precondition exists only to point
at the module inputs you wrote rather than at the resource attribute inside it.
Behaviours worth knowing before you plan:
- Empty sets are rejected, not treated as "no access". Both
account_access_custom_rolesandnamespace_accessesmust be omitted rather than passed as[]. The module validates this so the error arrives during plan rather than from the API. namespace_accessesis the complete map, not a set of additions. Anything granted outside Terraform is removed on the next apply, and removing an entry revokes that access.namespace_idis the fully qualified<namespace>.<account_id>form, not the bare namespace name. That is what the namespace module'snamespace_idoutput and thetemporalcloud_namespacesdata source both return.- Importing an existing user takes their ID, not their email.
terraform importagainst the user ID adopts a person who already accepted an invitation, which is the right way to bring an existing team under Terraform without re-inviting anyone. - Group membership is managed elsewhere. This module owns the user and their direct grants;
temporalcloud_group_membersbelongs to the group module and referencesuser_id.
- complete — every input, with the namespace and custom role the grants point at
- read-only — the narrowest useful grant, against an existing namespace
Both examples default to addresses on example.com, which
RFC 2606 reserves so it can never receive mail. They invite
nobody until you change that.
The wrappers submodule creates many users from one call, for use with Terragrunt or
anywhere a for_each on the module block is awkward:
module "users" {
source = "terraform-temporalcloud-modules/user/temporalcloud//wrappers"
version = "~> 2.0"
defaults = {
account_access = "read"
}
items = {
ana = { email = "ana@example.com", account_access = "developer" }
ben = { email = "ben@example.com" }
}
}Each key is one invitation, and removing a key revokes that person's access.
email and account_access are required, and the generated Inputs table below says so — they
carry no default because the provider marks both attributes required. What follows is what the table
cannot express.
create_user = false creates no user, but Terraform demands a value for a variable without a default
regardless of whether the resource it feeds exists. Pass empty strings:
module "user" {
# source and version as above
create_user = false
email = ""
account_access = ""
}Neither value reaches Temporal Cloud — the resource is counted out before they are used.
namespace_accessesmust be omitted whenaccount_accessisowneroradmin. Both roles hold Namespace Admin on every namespace already. The provider enforces this in its own schema validator —Users with account_access roles of owner or admin cannot have namespace accesses. Remove the namespace_accesses attribute.— and this module repeats it as a precondition so the message names the module input rather than the resource attribute inside it.- Every other role needs
namespace_accessesto reach a namespace at all. Account-level roles do not govern what happens inside a namespace:read,financeadminandmetricsreadcarry no namespace access, anddeveloperreceives Namespace Admin only on namespaces they create themselves. See Namespace-level permissions. Without an entry here — or membership of a group that has one — the user can sign in but cannot see a namespace's workflows. - Both keys of every
namespace_accessesentry are required.namespace_idandpermissionare required in the provider schema and in this module's object type, which the generated table cannot show: it lists the variable, not the keys inside it. Omitting one fails at validate withelement 0: attribute "permission" is required. - Empty sets are not the same as omission.
namespace_accessesandaccount_access_custom_rolesare each rejected as[], by this module and by the provider. Omit them.
The owner/admin rule is the one of these that terraform validate does not catch: it compares two
variables, so it lives in a resource precondition and is evaluated at plan. The rest are variable
validations and fail at validate, without credentials.
| Name | Version |
|---|---|
| terraform | >= 1.5.7 |
| temporalcloud | >= 1.6.0 |
| Name | Version |
|---|---|
| temporalcloud | >= 1.6.0 |
No modules.
| Name | Type |
|---|---|
| temporalcloud_user.this | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| account_access | Account-level role granted to the user: admin, developer, read, financeadmin or metricsread, matched case-insensitively. owner is accepted only when importing an existing owner — it cannot be created, changed or removed without Temporal support. Those six are the whole set: none is valid on temporalcloud_group_access but not on a user, though a SCIM-managed user can read back as none. admin and owner reach every namespace implicitly, so they cannot be combined with namespace_accesses. Pass "" when create_user is false, where no user is created and the value goes unused |
string |
n/a | yes |
| account_access_custom_roles | Optional. IDs of custom roles granted in addition to the built-in account_access role; left out, the user holds only that built-in role. A principal may be assigned at most 10 custom roles. Omit rather than passing an empty set |
set(string) |
null |
no |
| create_user | Controls if the user should be created. Set to false to disable the module without removing the call — email and account_access are required by Terraform either way, so pass "" for both when the module is switched off. Note that creating a user sends an invitation email to email, and destroying one revokes that person's access to the account |
bool |
true |
no |
Email address of the person to invite. Temporal Cloud sends an invitation to this address on create, and the person has to accept it before they can sign in. Changing this address replaces the user: the previous address loses access and a new invitation is sent. Pass "" when create_user is false, where no user is created and the value goes unused |
string |
n/a | yes | |
| namespace_accesses | Optional, and rejected outright when account_access is admin or owner — those roles reach every namespace implicitly. Per-namespace grants, as a set of entries whose namespace_id and permission are both required. permission is admin, write or read, matched case-insensitively. Other account roles carry no automatic namespace access — a developer gets it only on namespaces they create themselves — so a user who needs a namespace needs an entry here or a group grant. This set is the user's complete namespace access map, so removing an entry revokes that access. Omit rather than passing an empty set |
set(object({ |
null |
no |
| timeouts | Optional. Create and delete timeouts, as duration strings such as 30s or 2h45m. Left out, the provider's own defaults apply |
object({ |
{} |
no |
| Name | Description |
|---|---|
| user_account_access | The account-level role granted to the user |
| user_account_access_custom_roles | IDs of the custom roles granted to the user in addition to the built-in account role. Empty when none are assigned |
| user_email | The email address the invitation was sent to |
| user_id | The unique identifier of the user. This is the ID other resources reference, for example temporalcloud_group_members.users |
| user_namespace_accesses | The user's complete namespace access map, as namespace_id and permission pairs. Empty for account roles that reach every namespace implicitly |
| user_state | The provisioning state of the user record, as reported by Temporal Cloud: one of activating, active, updating, deleting, deleted, suspended, expired, or the activationfailed, updatefailed and deletefailed error states. This is the lifecycle of the record, not a signal of whether the person has accepted their invitation — check the Temporal Cloud UI or tcld user for that |
See CONTRIBUTING.md for the development workflow, how the test layers are arranged, and how the apply tests create real users without ever mailing a person.
Apache-2.0 licensed. See LICENSE.