Manages the full create/read/update/delete lifecycle of an Entra ID
userobject (Microsoft Graph v1.0usersendpoint) via a singlemsgraph_resource.this, targetingmicrosoft/msgraph0.3.0.
- π€ Provisions an Entra ID user via
POST /users, drift-detects and patches it thereafter, and deletes it onterraform destroy. - π§± Mirrors the Graph
userresource type's documented writable surface as a deeply-typedobject-free set of scalarvariables.tfinputs β every optional property istry(var.x, null)-rendered, so a caller cannot pass a malformed shape and still reach aplan. - π Never accepts or emits a real password. Graph requires a
passwordProfile.passwordvalue at create; this module satisfies that structural requirement with arandom_passwordthrowaway value that is invalidated before it can ever be used (see π§ Architecture Notes). - π« Defaults every new account to
accountEnabled = falseβ a newly provisioned user is inert until the identity team deliberately activates it. - π Its
idoutput is the reference type nearly every other module in this catalog will eventually consume (group membership/ownership, app role assignments, directory role assignments) β see πΊοΈ Where this fits.
π‘ Why it matters: this is the foundational identity primitive for the whole
msgraphmodule library. Nearly every other planned module β group membership, group ownership, app role assignment, directory role assignment, PIM eligibility β references a user by theidthis module emits. Getting its secure defaults and its password handling right here means every downstream module inherits a safe, non-secret-leaking foundation.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
This module has no upstream Consumes β it is a standalone, independently-lifecycled Graph entity. It does,
however, sit at the root of a real downstream consumer family. The catalog entries below are planned
per this module suite's module catalog table; as of this module's authoring, terraform-msgraph-user is the only module
directory that exists in this repository (confirmed β no other terraform-msgraph-* folders are present), so
the downstream boxes below represent future catalog coverage, not published, consumable modules yet.
flowchart LR
Graph["Microsoft Graph<br/>Entra ID tenant<br/>(POST/GET/PATCH/DELETE /users)"]:::external
User["terraform-msgraph-user<br/>(this module)"]:::thisModule
GM["terraform-msgraph-group-membership<br/>(planned catalog entry β not yet authored)"]:::sibling
GO["terraform-msgraph-group-owner<br/>(planned catalog entry β not yet authored)"]:::sibling
ARA["terraform-msgraph-app-role-assignment<br/>(planned catalog entry β not yet authored)"]:::sibling
DRA["terraform-msgraph-directory-role-assignment<br/>(planned catalog entry β not yet authored)"]:::sibling
User -->|"manages lifecycle of"| Graph
User -->|"id consumed as member principal"| GM
User -->|"id consumed as owner principal"| GO
User -->|"id consumed as assignee principal"| ARA
User -->|"id consumed as role-assignment principal"| DRA
classDef thisModule fill:#0078D4,color:#ffffff,stroke:#005A9E,stroke-width:2px
classDef sibling fill:#005A9E,color:#ffffff,stroke:#003D66,stroke-width:1px
classDef external fill:#EDEDED,color:#333333,stroke:#B0B0B0,stroke-width:1px
A single keystone plus one non-Graph helper resource β no for_each children, no nested composite blocks.
flowchart TD
subgraph Inputs["Caller inputs"]
I1["display_name, user_principal_name,<br/>mail_nickname (required)"]:::neutral
I2["account_enabled, given_name, surname,<br/>mail, usage_location, age_group, etc.<br/>(optional)"]:::neutral
end
RP["random_password.initial<br/>hashicorp/random β throwaway value,<br/>never a caller input or output"]:::neutral
This["msgraph_resource.this<br/>url = users<br/>ignore_missing_property = true"]:::thisModule
subgraph Outputs["Outputs"]
O1["id (primary)"]:::neutral
O2["display_name"]:::neutral
O3["user_principal_name"]:::neutral
end
I1 -->|"rendered into body"| This
I2 -->|"rendered into body via try x null"| This
RP -->|"result written once into passwordProfile.password"| This
This -->|"msgraph_resource.this.id"| O1
This -.->|"echoed from var.display_name"| O2
This -.->|"echoed from var.user_principal_name"| O3
classDef thisModule fill:#0078D4,color:#ffffff,stroke:#005A9E,stroke-width:2px
classDef neutral fill:#EDEDED,color:#333333,stroke:#B0B0B0,stroke-width:1px
Resource inventory
| Resource | Type | Role |
|---|---|---|
random_password.initial |
random_password |
Generates a 32-character throwaway value satisfying Graph's create-time passwordProfile.password requirement β never a real credential |
msgraph_resource.this |
msgraph_resource |
Keystone β full create/read/update/delete lifecycle of the Graph user at url = "users" |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
microsoft/msgraph |
0.3.0, pinned exactly (pre-1.0 provider β no ~> constraint) |
hashicorp/random |
~> 3.6 β companion provider, used solely for the throwaway create-time password |
| Graph API version | v1.0 (users endpoint) |
| Provider block | None in this module β the caller configures provider "msgraph" {} (auth, tenant, API version) in the root module |
Schema notes that bite
- This module never accepts or emits a real password β by design, not by omission. Graph genuinely
requires a
passwordProfile.passwordvalue at create, and themicrosoft/msgraphprovider'sbodyargument has no write-only/ephemeral mechanism to route around it (confirmed against the provider's own schema βbodyis a plainDynamicattribute).random_password.initialexists only to satisfy that structural requirement;passwordProfile.forceChangePasswordNextSignInis hardcodedtrueinmain.tf(not a variable), invalidating the generated value before it can ever be used to sign in. The identity team must establish the real initial credential out-of-band (SSPR, a Temporary Access Pass, or an admin-driven password reset) after provisioning. account_enableddefaults tofalse. A newly provisioned user is disabled until the caller explicitly setsaccount_enabled = true. Combined with the throwaway password above, a freshly created user is inert on both axes (unknown password, forced to change it; disabled) until the identity team deliberately activates it.userPrincipalName's domain must already be a verified domain in the tenant. This module'svalidation {}block only checks the UPN's shape (allowed local-part characters, a syntactically valid domain suffix) β it cannot check atplantime whether that domain is actually present in the tenant'sorganization.verifiedDomainscollection, since that requires a live Graph lookup. An unverified domain passesvalidate/plancleanly and fails only atapply.displayNamecan never be cleared once set. Plan an update, never a removal, if this ever needs to change.accountEnabledis subject to Graph's "sensitive actions" gating on update β only specific privileged administrator roles/permissions can change it after creation, independent of the base write permission.ignore_missing_property = trueonmsgraph_resource.thisis what keeps the create-only throwaway password from being silently re-sent (andforceChangePasswordNextSignInre-armed) on every futureapplyβ Graph'sGET /usersresponse never echoespasswordProfileback, so the provider treats it as legitimately absent rather than drifted.
| Operation | Application permission |
|---|---|
| Create | User.Create (least privilege); broader alternatives: User.ReadWrite.All, Directory.ReadWrite.All |
| Read | User.Read.All |
| Update | User.ReadUpdate.All (confirm current availability at authoring time β fallback User.ReadWrite.All) |
| Delete | User.ReadWrite.All β the least-privileged and only listed application permission for DELETE /users/{id} (no narrower alternative exists, unlike Create's User.Create). Deleting a user who holds a privileged administrator role additionally requires the calling app to hold a higher-privileged administrator directory role per Graph's "sensitive actions" gating β a directory-role prerequisite layered on top of the application permission, not a substitute for it. |
All application permissions above require admin consent.
- Graph API version: v1.0 β no beta dependency.
- License/SKU: none beyond baseline Entra ID.
usage_locationmust be set before a license SKU can be assigned to the user by a separate, not-yet-authored licensing-assignment module (out of this module's scope).customSecurityAttributesrequires an additionalCustomSecAttributeAssignment.ReadWrite.Allpermission grant beyond the base user-write permission; this module excludes that property from its schema entirely (see π§ Architecture Notes).- Admin consent: required for every application permission listed above.
terraform-msgraph-user/
βββ providers.tf # required_version >= 1.12.0; microsoft/msgraph pinned exactly at 0.3.0;
β # hashicorp/random ~> 3.6; no provider {} block for either
βββ variables.tf # 20 deeply-typed scalar inputs mirroring the Graph v1.0 user resource type
βββ main.tf # random_password.initial + msgraph_resource.this (keystone, url = "users")
βββ outputs.tf # id (primary), display_name, user_principal_name
βββ README.md # this file β includes this module's design intent, permissions, prerequisites,
β # and provider gotchas; a standalone SCOPE.md file has not yet been promoted
β # out of it as of this README
βββ examples/
βββ basic/
βββ main.tf # smallest real call β the four Graph-required properties only
terraform {
required_version = ">= 1.12.0"
required_providers {
msgraph = {
source = "microsoft/msgraph"
version = "0.3.0"
}
}
}
# Auth, tenant, and API version are configured here, by the caller β never inside this module.
provider "msgraph" {}
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Adele Vance"
user_principal_name = "AdeleV@contoso.com"
mail_nickname = "AdeleV"
# secure default (account_enabled = false) is left in place deliberately β the identity team
# activates the account out-of-band once onboarding/credential handoff is confirmed.
}
output "user_id" {
value = module.user.id
}Consumes: none β this module has no upstream dependency on another module in this catalog.
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Graph object id (GUID) of the created user β primary output | Catalog modules that reference a user principal by id: group-membership, group-owner, app-role-assignment, directory-role-assignment. These are planned entries in this module suite's module catalog table β not yet authored as .tf files in this repository as of this module's authoring. |
display_name |
The user's displayName, as provided to this module |
Callers needing a human-readable label for logs, notifications, or downstream naming |
user_principal_name |
The user's userPrincipalName, as provided to this module |
Callers needing the sign-in identifier, e.g. downstream onboarding/licensing automation outside this module's scope |
1 Β· Minimal required call
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Adele Vance"
user_principal_name = "AdeleV@contoso.com"
mail_nickname = "AdeleV"
}βΉοΈ Only the four Graph-required properties are set.
account_enabledis left at itsfalsesecure default; every other optional property isnull.
2 Β· Provisioning an already-active account (opting out of the disabled default)
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Nestor Wilke"
user_principal_name = "NestorW@contoso.com"
mail_nickname = "NestorW"
account_enabled = true
}
β οΈ Only passaccount_enabled = truewhen the identity team has already confirmed a credential handoff path exists β the module's throwaway create-time password is never usable to sign in (forceChangePasswordNextSignInis alwaystrue).
3 Β· Personal directory attributes
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Megan Bowen"
user_principal_name = "MeganB@contoso.com"
mail_nickname = "MeganB"
given_name = "Megan"
surname = "Bowen"
job_title = "Director of Finance"
department = "Finance"
}4 Β· Contact properties
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Alex Wilber"
user_principal_name = "AlexW@contoso.com"
mail_nickname = "AlexW"
mail = "AlexW@contoso.com"
mobile_phone = "+1 555 0130"
office_location = "Building 3, Floor 2"
}5 Β· Company and employment metadata
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Diego Siciliani"
user_principal_name = "DiegoS@contoso.com"
mail_nickname = "DiegoS"
company_name = "Contoso Ltd."
employee_id = "E-10492"
employee_type = "Employee"
employee_hire_date = "2026-08-01T00:00:00Z"
}βΉοΈ
employee_typeis free text β Graph does not document it as a closed enum, so this module applies novalidation {}block to it.
6 Β· Setting usage_location ahead of a future license assignment
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Isaiah Langer"
user_principal_name = "IsaiahL@contoso.com"
mail_nickname = "IsaiahL"
usage_location = "US"
}π‘
usage_locationis required by Graph before a license SKU can be assigned to this user by a separate licensing-assignment module (out of this module's scope) β service availability by country/region is a legal requirement, not a Graph technicality.
7 Β· Provisioning a minor account
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Casey Jensen"
user_principal_name = "CaseyJ@contoso.com"
mail_nickname = "CaseyJ"
age_group = "Minor"
consent_provided_for_minor = "Granted"
}π Both
age_groupandconsent_provided_for_minorare validated against Graph's documented closed enums (Minor/NotAdult/AdultandGranted/Denied/NotRequiredrespectively) β an out-of-range value fails atplan, before any Graph call is made.
8 Β· Provisioning a federated / hybrid-synced user
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Patti Fernandez"
user_principal_name = "PattiF@fabrikam.com"
mail_nickname = "PattiF"
on_premises_immutable_id = "a1B2c3D4e5F6g7H8i9J0=="
}
β οΈ on_premises_immutable_idis only meaningful whenuser_principal_nameuses a federated domain synced from on-premises Active Directory. Leave itnull(the default) for cloud-only users.
9 Β· Preferred language
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Grady Archie"
user_principal_name = "GradyA@contoso.com"
mail_nickname = "GradyA"
preferred_language = "en-US"
}10 Β· Realistic onboarding call combining several optional attributes
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Lynne Robbins"
user_principal_name = "LynneR@contoso.com"
mail_nickname = "LynneR"
given_name = "Lynne"
surname = "Robbins"
job_title = "Retail Manager"
department = "Retail"
company_name = "Contoso Ltd."
office_location = "Store 204"
mobile_phone = "+1 555 0199"
usage_location = "US"
employee_id = "E-20117"
employee_type = "Employee"
employee_hire_date = "2026-09-15T00:00:00Z"
}11 Β· for_each at scale β creating multiple users from a map
variable "new_hires" {
type = map(object({
display_name = string
user_principal_name = string
mail_nickname = string
department = string
}))
}
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
for_each = var.new_hires
display_name = each.value.display_name
user_principal_name = each.value.user_principal_name
mail_nickname = each.value.mail_nickname
department = each.value.department
}
output "new_hire_ids" {
value = { for k, m in module.user : k => m.id }
}π‘ Keying by a stable map key (e.g. an HR system employee number) rather than
countmeans adding or removing one new hire fromvar.new_hiresnever forces Terraform to re-index β and therefore never re-plans β every other user in the map.
12 Β· Least-privilege permissions callout
# Root module β provider auth configured with the narrowest application permission set this module
# actually needs for a create-only pipeline: User.Create (not User.ReadWrite.All or
# Directory.ReadWrite.All). Confirm the running principal's app registration is granted exactly this
# scope, with admin consent, before applying.
provider "msgraph" {}
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Irvin Sayle"
user_principal_name = "IrvinS@contoso.com"
mail_nickname = "IrvinS"
}π See π Graph API Permissions Required β a pipeline that only ever creates users needs
User.Create, not the broaderUser.ReadWrite.All/Directory.ReadWrite.Allalternatives. Reserve the broader grants for pipelines that also update or delete users.
13 Β· Future-dated hire
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Miriam Graham"
user_principal_name = "MiriamG@contoso.com"
mail_nickname = "MiriamG"
account_enabled = false
employee_hire_date = "2026-11-02T00:00:00Z"
}βΉοΈ
employee_hire_dateaccepts a future timestamp β Graph models this as "hired or will start work in a future hire." Leavingaccount_enabledat itsfalsedefault here is deliberate: the account can be provisioned ahead of the start date without being sign-in-capable until the identity team activates it.
14 Β· A validation failure this module catches before any Graph call
module "user" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Bad Example"
user_principal_name = "bad user!!@contoso.com" # fails var.user_principal_name's validation{}
mail_nickname = "BadExample"
}
β οΈ This failsterraform validate/plan, notapplyβuser_principal_name's local part contains a space, which is not among the characters Graph allows (A-Z a-z 0-9 '. - _ ! # ^ ~). Compare this to π§ͺ Testing, below: shape-level mistakes like this one are caught for free; a UPN whose domain is unverified in the tenant is not, because that requires a live Graph lookup this module cannot perform atplantime.
15 Β· ποΈ End-to-end composition β a user consumed by downstream catalog modules
This module has no upstream Consumes, so its mandatory end-to-end example instead shows the realistic
downstream chain: a user provisioned here, then referenced by id from two catalog modules that are
planned but not yet authored as of this README (terraform-msgraph-group-owner and
terraform-msgraph-app-role-assignment β see this module suite's module catalog table). The shape below is
illustrative of the intended contract, not a call against a published module today.
module "engineering_lead" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"
display_name = "Adele Vance"
user_principal_name = "AdeleV@contoso.com"
mail_nickname = "AdeleV"
account_enabled = true
job_title = "Engineering Lead"
department = "Engineering"
}
# Illustrative only β terraform-msgraph-group-owner is a planned catalog entry, not yet authored.
# module "engineering_group_owner" {
# source = "git::https://github.com/microsoftexpert/terraform-msgraph-group-owner.git?ref=v1.0.0"
#
# group_id = module.engineering_group.id
# principal_id = module.engineering_lead.id
# }
# Illustrative only β terraform-msgraph-app-role-assignment is a planned catalog entry, not yet authored.
# module "engineering_lead_app_role" {
# source = "git::https://github.com/microsoftexpert/terraform-msgraph-app-role-assignment.git?ref=v1.0.0"
#
# service_principal_id = module.internal_tools_sp.id
# principal_id = module.engineering_lead.id
# app_role_id = var.internal_tools_admin_role_id
# }π‘ Once
group-ownerandapp-role-assignmentare authored, the ordering shown here is exactly what Terraform's own dependency graph enforces without anydepends_on: both downstream calls takemodule.engineering_lead.idas an input, so Terraform will not attempt either assignment before the user exists.
Grouped summary
| Group | Variables |
|---|---|
| Required at create | display_name, user_principal_name, mail_nickname |
| Account state | account_enabled (default false) |
| Personal / organizational | given_name, surname, job_title, department, company_name, employee_id, employee_type, employee_hire_date, office_location |
| Contact | mail, mobile_phone |
| Locale / licensing prerequisite | preferred_language, usage_location |
| Hybrid identity | on_premises_immutable_id |
| Minor-account compliance | age_group, consent_provided_for_minor |
Full variable reference (20 inputs)
| Variable | Type | Default | Validation | Graph property |
|---|---|---|---|---|
display_name |
string |
β (required) | 1β256 characters | displayName |
user_principal_name |
string |
β (required) | Regex: local part limited to A-Z a-z 0-9 '. - _ ! # ^ ~, syntactically valid domain suffix |
userPrincipalName |
mail_nickname |
string |
β (required) | Non-empty | mailNickname |
account_enabled |
bool |
false |
none | accountEnabled |
given_name |
string |
null |
none (documented max 64 chars, not enforced) | givenName |
surname |
string |
null |
none (documented max 64 chars, not enforced) | surname |
mail |
string |
null |
none | mail |
mobile_phone |
string |
null |
none | mobilePhone |
job_title |
string |
null |
none (documented max 128 chars, not enforced) | jobTitle |
department |
string |
null |
none (documented max 64 chars, not enforced) | department |
company_name |
string |
null |
none (documented max 64 chars, not enforced) | companyName |
employee_id |
string |
null |
β€ 16 characters | employeeId |
employee_type |
string |
null |
none β free text, not a closed enum | employeeType |
employee_hire_date |
string |
null |
Must parse as an ISO 8601 / RFC 3339 timestamp (formatdate-checked) |
employeeHireDate |
office_location |
string |
null |
none | officeLocation |
preferred_language |
string |
null |
none | preferredLanguage |
usage_location |
string |
null |
Regex: two uppercase letters | usageLocation |
on_premises_immutable_id |
string |
null |
none | onPremisesImmutableId |
age_group |
string |
null |
One of "Minor", "NotAdult", "Adult", or null |
ageGroup |
consent_provided_for_minor |
string |
null |
One of "Granted", "Denied", "NotRequired", or null |
consentProvidedForMinor |
Deliberately excluded from this schema (see π§ Architecture Notes and π§± Design Principles below):
passwordProfile.password (secret value β never a module input), identities (B2B/B2C-only creation
path, out of scope), customSecurityAttributes (open complex type, tenant-specific, requires an
additional permission scope beyond this module's base scope), creationType (read-only, computed by
Graph).
| Output | Description | Sensitive / excluded? |
|---|---|---|
id |
Graph object id (GUID) of the created user β primary output | No |
display_name |
The user's displayName, as provided to this module |
No |
user_principal_name |
The user's userPrincipalName, as provided to this module |
No |
No output derived from random_password.initial exists anywhere in outputs.tf β the throwaway password
value is never surfaced, not even marked sensitive = true; it is excluded entirely.
random_password.initialis a structural placeholder, not a credential-management feature. It exists becausemsgraph_resource'sbodyargument has no write-only/ephemeral sub-key support (confirmed against the provider's own schema) and Graph genuinely requires apasswordProfile.passwordvalue at create. Itsresultis written intobody.passwordProfile.passwordexactly once, at create; the value is invalidated for sign-in purposes becausepasswordProfile.forceChangePasswordNextSignInis hardcodedtrueinmain.tf, not a variable. The value still lands in Terraform state, identical to anyrandom_passwordusage elsewhere in the Terraform ecosystem β an accepted, industry-standard trade-off, not a violation of this library's "never accept/emit a secret" rule, since the value is never caller-supplied and never module-output.ignore_missing_property = trueonmsgraph_resource.thisis load-bearing, not decorative. Graph'sGET /usersresponse never echoespasswordProfileback; without this setting, the provider could attempt to reconcile a "missing" property on every futureapply, re-sending the throwaway password and re-armingforceChangePasswordNextSignInwhenever an unrelated property (e.g.jobTitle) changes. This is preferred overlifecycle.ignore_changes, which would also block legitimate future updates topasswordProfile.accountEnabledupdates are gated by Graph's "sensitive actions" restrictions independent of the base write permission β only specific privileged administrator roles can flip this property after creation. Aplanthat looks clean can still fail atapplywith a 403 if the calling principal lacks that directory role.for_each, nevercount, is the pattern this module's example library uses for scaling to multiple users (see Example 11) β even though this module itself has no internalfor_eachchildren, keying by a stable map key at the caller's call site avoids re-indexing every other user when one is added or removed.
| Concern | Secure default in this module | Opt-out (caller must be explicit) |
|---|---|---|
Account activation (accountEnabled) |
account_enabled defaults to false β new users are provisioned disabled |
Caller sets account_enabled = true explicitly |
Create-time credential (passwordProfile.password) |
Never accepted as an input, never emitted as an output β a hard rule, not a toggle. random_password.initial supplies a throwaway value; forceChangePasswordNextSignIn is hardcoded true |
None β there is no opt-out. The real initial credential is always established out-of-band (SSPR, a Temporary Access Pass, or an admin-driven reset) |
customSecurityAttributes |
Excluded from this module's schema entirely (tenant-specific open complex type; requires an additional permission scope) | Not available in this module β manage via a dedicated call outside this module's scope |
| Sensitive outputs | No output is ever derived from a credential-bearing property | N/A β exclusion, not merely sensitive = true |
terraform init -backend=false
terraform validate
terraform fmt -checkPin every real consumption of this module to an explicit tag, never a branch:
source = "git::https://github.com/microsoftexpert/terraform-msgraph-user.git?ref=v1.0.0"terraform validate and terraform fmt -check prove the offline contract: every required property is
present, every documented closed enum (age_group, consent_provided_for_minor) is restricted to its
legal values, and shape-level regexes (user_principal_name's allowed character set, usage_location's
two-letter format, employee_hire_date's RFC 3339 parseability) reject obviously malformed input before any
Graph call is made.
What this cannot catch: msgraph_resource.this's body argument is a generic map from the provider's
own perspective β the provider does not know what a Graph user is. Concretely, in this module:
usage_location = "XX"satisfies the^[A-Z]{2}$shape regex and passesvalidate/plancleanly, but"XX"is not a real ISO 3166-1 alpha-2 country code β Graph rejects it only atapply, with a 400.user_principal_name = "AdeleV@notaverifieddomain.com"satisfies the UPN shape regex entirely, but fails only atapplyifnotaverifieddomain.comis not present in the tenant'sorganization.verifiedDomainscollection β this cannot be checked atplantime since it requires a live tenant lookup.company_name,given_name,surname,job_title, anddepartmentcarry documented Graph maximum lengths (64, 64, 64, 128, and 64 characters respectively) that this module does not enforce with avalidation {}block β an over-length value passesplanand is rejected only atapply.
This module's object-free scalar typing and validation {} blocks are the only thing standing between
the caller and a Graph 400 error at apply time for anything beyond these three categories.
$ terraform output
id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
display_name = "Adele Vance"
user_principal_name = "AdeleV@contoso.com"
No password value is ever present in terraform output, terraform show, or any other output surface of
this module.
| Symptom | Cause | Fix |
|---|---|---|
apply fails on create with a 400 referencing an invalid domain |
user_principal_name's domain is not a verified domain in the tenant's organization.verifiedDomains collection |
Add and verify the domain in Entra ID before applying, or change user_principal_name to an already-verified domain |
apply fails on an accountEnabled update with a 403 referencing sensitive actions |
The calling principal is not assigned a directory role Graph requires for its "sensitive actions" gating on accountEnabled |
Assign the calling app/service principal a sufficiently privileged administrator role, or have a privileged admin perform this specific update |
apply fails with a 400 referencing an invalid usageLocation |
usage_location matched the two-uppercase-letter shape regex but is not a real ISO 3166-1 alpha-2 country code |
Confirm the value against the current ISO 3166-1 alpha-2 list before setting it β this cannot be caught at plan time |
Plan shows an unexpected diff touching passwordProfile on an otherwise unrelated change |
ignore_missing_property missing or the provider version has drifted from the exact 0.3.0 pin |
Confirm providers.tf pins microsoft/msgraph at exactly 0.3.0 and that ignore_missing_property = true is present on msgraph_resource.this |
| New user is created but no one can sign in | Expected β by design. random_password.initial is a throwaway value invalidated by forceChangePasswordNextSignIn = true |
The identity team must establish the real credential out-of-band (SSPR, a Temporary Access Pass, or an admin-driven reset) |
apply is slow to reflect a just-created user in a dependent read (e.g. a data source lookup immediately after) |
Graph's Entra ID write path is not always immediately read-consistent | Re-run plan/apply, or add an explicit wait/retry in the consuming module rather than assuming instant consistency |
- Graph API reference β
userresource type: https://learn.microsoft.com/graph/api/resources/user?view=graph-rest-1.0 - Graph API reference β create user: https://learn.microsoft.com/graph/api/user-post-users?view=graph-rest-1.0
- Graph API reference β delete user (permissions table): https://learn.microsoft.com/graph/api/user-delete?view=graph-rest-1.0#permissions
- Graph API reference β
passwordProfileresource type: https://learn.microsoft.com/graph/api/resources/passwordprofile?view=graph-rest-1.0 - Graph API reference β who can perform sensitive actions: https://learn.microsoft.com/graph/api/resources/users?view=graph-rest-1.0#who-can-perform-sensitive-actions
- Provider docs β
microsoft/msgraph0.3.0: https://registry.terraform.io/providers/microsoft/msgraph/0.3.0/docs - This module's design record β documented inline in this README (a standalone
SCOPE.mdfile has not yet been promoted out of it as of this README)