Manages the full create/read/update/delete lifecycle of a Microsoft Entra ID Governance entitlement management access package catalog (Microsoft Graph v1.0
identityGovernance/entitlementManagement/catalogsendpoint) — the container that groups resources (groups, applications, SharePoint sites) available for inclusion in access packages — via a singlemsgraph_resource.this, targetingmicrosoft/msgraph0.3.0.
- 📦 Provisions an access package catalog — a container for zero or more access packages plus their linked
resources — via
POST /identityGovernance/entitlementManagement/catalogs, drift-detects and patches it thereafter, and deletes it onterraform destroy. - 🌱 This is a new standalone module created by a human-approved 3-way split of the original single
"access-package" catalog row in this suite's module table, since a catalog has its own independent
create/read/update/delete lifecycle and is consumed by many
access-packagemodules (one catalog can back many packages). Its two siblings from that split —access-package(the catalog-item entity) andaccess-package-assignment-policy(request/approval policy) — are separate modules, not folded in here. - 🔒 Secure by default on two fields Graph itself leaves ambiguous or permissive.
statedefaults to"unpublished"andis_externally_visibledefaults tofalse, both set explicitly at the Terraform level regardless of what Graph's own create-time behavior would otherwise do — see 🧠 Architecture Notes and 🧱 Design Principles. - 🚫
catalogTypeis never a caller-settable input. It is system-set by Graph to"userManaged"on creation via this endpoint and surfaced only as a computedcatalog_typeoutput, read back through the provider'sresponse_export_valuesmechanism — the same patternauthentication-strength-policyuses forpolicyType. - 🆔 Its
idoutput is the reference type theaccess-packagemodule's plannedcatalog_idinput consumes — see 🗺️ Where this fits for the current state of that sibling module.
💡 Why it matters: every access package in an Entra entitlement management deployment must belong to a catalog. Getting a catalog's secure defaults right here — unpublished until deliberately published, not externally visible until deliberately opened up — is what keeps a freshly-provisioned catalog from being immediately requestable by users (internal or external) before an administrator has actually finished configuring what it should contain.
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 of its own — like group or named-location, it is a foundational,
root-of-family entity in this catalog. Its one real downstream consumer is tf-mod-msgraph-access-package,
which takes this module's id as its catalog_id input (confirmed against
tf-mod-msgraph-access-package/SCOPE.md's "Consumes" table: catalog_id | string | access-package-catalog module output id).
As of this module's authoring, tf-mod-msgraph-access-package has a written SCOPE.md and a scaffolded
providers.tf, but no variables.tf, main.tf, or outputs.tf yet (confirmed — its directory contains
SCOPE.md, scaffold files, and providers.tf only) — it is not yet a real, callable module.
The family DAG below shows this edge as planned, per this module's authoring instructions, rather than
inventing others or overstating the sibling's current state.
flowchart LR
APC["tf-mod-msgraph-access-package-catalog<br/>(this module)"]:::thisModule
AP["tf-mod-msgraph-access-package<br/>(SCOPE.md + providers.tf scaffolded —<br/>variables.tf/main.tf/outputs.tf<br/>not yet authored)"]:::sibling
Graph["Microsoft Graph<br/>Entra ID Governance tenant<br/>(POST/GET/PATCH/DELETE<br/>/identityGovernance/entitlementManagement/catalogs)"]:::external
APC -->|"manages lifecycle of"| Graph
APC -->|"id consumed as catalog_id input"| AP
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
Validated via the Mermaid Chart MCP before embedding (valid: true).
A single keystone resource — no for_each children, no owned nested composite blocks.
flowchart TD
subgraph Inputs["Caller inputs"]
I1["display_name (required)"]:::neutral
I2["description (optional, default null)"]:::neutral
I3["state: 'unpublished' | 'published'<br/>(default 'unpublished' — secure default)"]:::neutral
I4["is_externally_visible (default false —<br/>secure default)"]:::neutral
end
This["msgraph_resource.this<br/>url = identityGovernance/entitlementManagement/catalogs<br/>response_export_values: catalogType"]:::thisModule
subgraph Outputs["Outputs"]
O1["id (primary)"]:::neutral
O2["display_name"]:::neutral
O3["state"]:::neutral
O4["catalog_type (computed-only,<br/>never a caller input)"]:::neutral
end
I1 -->|"rendered into body.displayName"| This
I2 -->|"rendered into body.description"| This
I3 -->|"rendered into body.state"| This
I4 -->|"rendered into body.isExternallyVisible"| This
This -->|"msgraph_resource.this.id"| O1
This -.->|"echoed from var.display_name"| O2
This -.->|"echoed from var.state"| O3
This -->|"read back via response_export_values.catalogType"| O4
classDef thisModule fill:#0078D4,color:#ffffff,stroke:#005A9E,stroke-width:2px
classDef neutral fill:#EDEDED,color:#333333,stroke:#B0B0B0,stroke-width:1px
Validated via the Mermaid Chart MCP before embedding (valid: true).
Resource inventory
| Resource | Type | Role |
|---|---|---|
msgraph_resource.this |
msgraph_resource |
Keystone — full create/read/update/delete lifecycle of the Graph access package catalog at url = "identityGovernance/entitlementManagement/catalogs", with response_export_values reading back the Graph-computed catalogType property |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
microsoft/msgraph |
0.3.0, pinned exactly (pre-1.0 provider — no ~> constraint) |
| Graph API version | v1.0 (identityGovernance/entitlementManagement/catalogs endpoint) |
| Provider block | None in this module — the caller configures provider "msgraph" {} (auth, tenant, API version) in the root module |
Schema notes that bite
catalogTypeis not on the Create request-body table at all, even though it appears on the Get/Update reference pages and the general resource-type page. Every documented Create example's201response echoescatalogType = "userManaged"without the caller ever supplying it. This module never exposes it as an input — see 🧠 Architecture Notes.state's documented enum is wider on the general resource-type page (unpublished/published/unknownFutureValue) than on the live Create/Update request-body pages (unpublished/publishedonly, stated verbatim as "The possible values are: unpublished and published"). This module'svalidation {}restricts to the narrower, request-body-specific two values — see 🧠 Architecture Notes for the full rationale on this judgment call.statehas no documented default if omitted from Create — the request-body table lists no fallback, and every documented Create example passes it explicitly. This module does not rely on that ambiguity: it defaults to"unpublished"at the Terraform level on every apply, an explicit secure-default choice.catalog(the field this catalog is referenced as, from the access-package entity) is documented as force-new on that sibling entity, not on this one — this module's own catalog entity has no comparable immutable reference field of its own; every property in scope here (displayName,description,state,isExternallyVisible) is confirmed mutable via ordinaryPATCHon the live "Update accessPackageCatalog" reference.- The Delete reference page is silent on any "catalog must be empty" precondition — unlike the sibling
access-packagemodule's explicitly documented "cannot delete with assignments" rule. Treat emptying the catalog before deleting it as safe operational practice, not a confirmed API guarantee — see 🔍 Troubleshooting.
| Operation | Application permission |
|---|---|
| Create / Update / Delete | EntitlementManagement.ReadWrite.All (least-privileged Entra role: Catalog creator for create, Catalog owner for update/delete) |
| Read / List | EntitlementManagement.Read.All — confirmed via the live "Get accessPackageCatalog" reference page (least-privileged application permission; higher-privileged alternative EntitlementManagement.ReadWrite.All; least-privileged Entra role: Catalog reader). Source: https://learn.microsoft.com/graph/api/accesspackagecatalog-get?view=graph-rest-1.0#permissions |
All application permissions above require admin consent.
- Graph API version: v1.0 (
identityGovernance/entitlementManagement/catalogs). - License/SKU: Microsoft Entra ID Governance (member users); some capabilities also operate under Entra ID P2 — confirm current SKU naming/bundling against Microsoft Learn before publishing.
- Admin consent: required for
EntitlementManagement.ReadWrite.All.
tf-mod-msgraph-access-package-catalog/
├── providers.tf # required_version >= 1.12.0; microsoft/msgraph pinned exactly at 0.3.0;
│ # no provider {} block
├── variables.tf # 4 deeply-typed inputs mirroring the Graph v1.0 accessPackageCatalog
│ # resource type's Create-time writable surface
├── main.tf # msgraph_resource.this (keystone), with response_export_values reading back
│ # the Graph-computed catalogType property
├── outputs.tf # id (primary), display_name, state, catalog_type (computed-only)
├── README.md # this file
└── examples/
└── basic/
└── main.tf # smallest real call — a catalog with only display_name set
ℹ️ This is a standalone module per this suite's SCOPE.md convention — its scope is lightweight and remains embedded in this module's design record rather than promoted to a separate
SCOPE.mdfile.
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 "sales_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Sales Partner Resources"
}
output "sales_catalog_id" {
value = module.sales_catalog.id
}Consumes
| Input | Type | Source module |
|---|---|---|
| (none) | — | access-package-catalog is a foundational entity with no upstream Consumes of its own in this catalog. |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Graph object id (GUID) of the created access package catalog — primary output | access-package's catalog_id input (planned catalog entry — SCOPE.md written, .tf files not yet authored; see 🗺️ Where this fits) |
display_name |
The catalog's displayName, as provided to this module |
Callers needing a human-readable label for logs or downstream naming |
state |
The catalog's state ("unpublished"/"published"), as provided to this module |
Callers needing to confirm publication status without a separate data source read |
catalog_type |
Graph-computed catalogType (userManaged/serviceDefault/serviceManaged/unknownFutureValue), read back via response_export_values — always "userManaged" for a catalog this module creates |
Informational; not currently consumed by any sibling module |
1 · Minimal catalog — display name only
module "sales_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Sales Partner Resources"
}ℹ️
descriptionis leftnull,statedefaults to"unpublished", andis_externally_visibledefaults tofalse— the safest possible catalog: not yet available for management and not externally requestable.
2 · Catalog with a description
module "engineering_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Engineering Team Resources"
description = "Groups and applications used by the engineering organization"
}3 · Publishing a catalog explicitly
module "published_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "General Employee Resources"
description = "Published catalog — access packages here are immediately available for management"
state = "published"
}
⚠️ state = "published"must be set explicitly — this module's secure default is"unpublished"regardless of Graph's own undocumented Create-time behavior. See 🧠 Architecture Notes.
4 · Externally visible catalog
module "partner_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "External Partner Resources"
description = "Resources requestable by users outside this tenant"
state = "published"
is_externally_visible = true
}🔒
is_externally_visible = truemust be set explicitly — this module's secure default isfalse, matching Graph's own confirmed default. See 🧱 Design Principles.
5 · Unpublished catalog awaiting content
module "in_progress_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Q3 Reorg — Resources Pending Review"
description = "Do not publish until resource onboarding is complete"
# state left at its default, "unpublished"
}6 · Reading the computed catalog_type output
module "hr_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "HR Systems Access"
}
output "hr_catalog_type" {
value = module.hr_catalog.catalog_type # always "userManaged" for a catalog this module creates
}ℹ️
catalog_typeis never a caller input — it's read back from Graph's create response viaresponse_export_valuesand is always"userManaged"for any catalog this module provisions.
7 · A validation failure this module catches before any Graph call (invalid state)
module "broken_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Broken Example"
state = "Published" # fails — case-sensitive, and not one of the two legal values
}
⚠️ This failsterraform plan, notapply—var.state'svalidation {}block only accepts the exact strings"unpublished"and"published"(see 🧪 Testing).
8 · A validation failure this module catches before any Graph call (empty display name)
module "broken_catalog_2" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "" # fails — must be non-empty
}9 · for_each at scale — creating multiple catalogs from a map
variable "department_catalogs" {
type = map(object({
display_name = string
description = optional(string)
}))
}
module "department_catalogs" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
for_each = var.department_catalogs
display_name = each.value.display_name
description = each.value.description
}
output "department_catalog_ids" {
value = { for k, m in module.department_catalogs : k => m.id }
}💡 Keying by a stable map key (e.g. a department code) rather than
countmeans adding or removing one department fromvar.department_catalogsnever re-indexes — and therefore never re-plans — every other catalog in the map.
10 · Least-privilege permissions callout
# Root module — provider auth configured with exactly the application permission this module's write
# path needs: EntitlementManagement.ReadWrite.All. Confirm the running principal's app registration is
# granted it, with admin consent, before applying.
provider "msgraph" {}
module "compliance_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Compliance Reporting Access"
}🔒 See 🔑 Graph API Permissions Required — read-only automation (drift detection, data sources) can run against the narrower
EntitlementManagement.Read.Allinstead.
11 · Renaming an existing catalog (ordinary PATCH, not force-new)
module "renamed_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Sales & Partner Resources" # changed from "Sales Partner Resources"
description = "Renamed to reflect broadened scope"
}ℹ️
displayName,description,state, andisExternallyVisibleare all confirmed mutable via ordinaryPATCHon the live "Update accessPackageCatalog" reference — none of this module's in-scope properties are force-new.
12 · Moving from unpublished to published as a deliberate two-step rollout
# Step 1 — provision unpublished while resources are onboarded to the catalog out of band.
module "rollout_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "New Vendor Onboarding"
# state left at its default, "unpublished"
}
# Step 2 — once resources are confirmed onboarded, flip state explicitly in a later apply.
# module "rollout_catalog" {
# source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
#
# display_name = "New Vendor Onboarding"
# state = "published"
# }💡 This module's
unpublished-by-default posture is designed for exactly this two-step rollout pattern — provision the container first, confirm its contents are correct, then explicitly publish.
13 · Multiple catalogs with mixed visibility
module "internal_only_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Internal Only Resources"
state = "published"
# is_externally_visible left at its default, false
}
module "external_facing_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "External-Facing Resources"
state = "published"
is_externally_visible = true
}14 · Realistic combined call — a fully-described, published catalog
module "finance_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Finance Systems Access"
description = "Groups and applications used by Finance and Accounting"
state = "published"
# is_externally_visible left at its default, false — internal-only
}15 · 🏗️ End-to-end composition — this catalog's id feeding the planned access-package module
tf-mod-msgraph-access-package has a written SCOPE.md (confirmed: its Consumes table lists catalog_id | string | access-package-catalog module output id) and a scaffolded providers.tf, but no
variables.tf/main.tf/outputs.tf yet as of this README — the module call below is illustrative of the
intended contract, not a call against a published module today. The commented-out block shows exactly how
this module's downstream consumer is meant to wire this module's id output once access-package is fully
authored, per this catalog's Consumes/Emits convention (see this suite's "Worked cross-module
composition example").
module "partner_resources_catalog" {
source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Partner Resources"
description = "Catalog backing the partner onboarding access package"
state = "published"
}
# Illustrative only — tf-mod-msgraph-access-package has a written SCOPE.md and a scaffolded providers.tf,
# but no variables.tf/main.tf/outputs.tf as of this README.
# module "partner_onboarding_package" {
# source = "git::https://github.com/microsoftexpert/tf-mod-msgraph-access-package.git?ref=v1.0.0"
#
# display_name = "Partner Onboarding"
# catalog_id = module.partner_resources_catalog.id
# #... resource_role_scopes, etc.
# }
output "partner_resources_catalog_id" {
value = module.partner_resources_catalog.id
}💡 Once
access-packageis fully authored, the ordering shown here is exactly what Terraform's own dependency graph enforces without anydepends_on: the package'scatalog_idinput takesmodule.partner_resources_catalog.idas a real output reference, so Terraform will not attempt to create the access package before its catalog exists. If the catalog is ever destroyed while packages still exist in it, Graph's own delete-precondition behavior for this entity is undocumented (see 🔍 Troubleshooting) — emptying the catalog first is the safe operational practice, not a confirmed API guarantee.
Grouped summary
| Group | Variables |
|---|---|
| Required at create | display_name |
| Optional, secure-defaulted | state (default "unpublished"), is_externally_visible (default false) |
| Optional, no security implication | description (default null) |
Full variable reference (4 inputs)
| Variable | Type | Default | Validation | Graph property |
|---|---|---|---|---|
display_name |
string |
— (required) | Non-empty | displayName |
description |
string |
null |
None | description |
state |
string |
"unpublished" (secure default) |
One of "unpublished", "published" |
state |
is_externally_visible |
bool |
false (secure default, also matches Graph's own confirmed default) |
None (type-checked bool) | isExternallyVisible |
Deliberately excluded from this schema (read-only/computed, or explicitly not exposed per this module's
embedded SCOPE.md — never settable inputs): id (surfaces only as msgraph_resource.this.id),
createdDateTime, modifiedDateTime, catalogType (system-set by Graph on creation; surfaced only as the
computed catalog_type output — see 🧠 Architecture Notes for the full rationale, including why this module
does not wire in the update-time catalogType path the live Update reference page lists).
| Output | Description | Sensitive / excluded? |
|---|---|---|
id |
Graph object id (GUID) of the created access package catalog — primary output | No |
display_name |
The catalog's displayName, as provided to this module |
No |
state |
The catalog's state ("unpublished"/"published"), as provided to this module |
No |
catalog_type |
Graph-computed catalogType, read back via response_export_values — always "userManaged" for a catalog this module creates |
No |
No output is derived from a credential-bearing property — no property on the accessPackageCatalog
resource type carries a secret value, so there is nothing secret-shaped to exclude at the output stage.
catalogTypeis deliberately never a caller input, even though a fresh Microsoft Learn fetch of the Update page during this authoring session confirmscatalogTypeis technically listed as an updatable property onPATCH /identityGovernance/entitlementManagement/catalogs/{id}. This module still excludes it fromvariables.tfentirely, per this module's embeddedSCOPE.mdand the explicit authoring instruction behind it: the Create request-body table does not list it at all (every documented Create example's response echoescatalogType = "userManaged"without the caller supplying it), and re-designating an existing catalog'suserManaged/serviceDefault/serviceManagedclassification after the fact is not a normal operational pattern — Graph's own documentation gives no guidance on what such a change would do to a catalog already in active use. It is surfaced only as a computedcatalog_typeoutput, read back viaresponse_export_values— the same mechanismtf-mod-msgraph-authentication-strength-policyuses for its own read-onlypolicyType/requirementsSatisfiedproperties.state's legal-value set is deliberately narrower than the general resource-type page's documented enum. TheaccessPackageCatalogStatetype on the generalaccessPackageCatalogresource-type reference page documents three values (unpublished,published,unknownFutureValue), but both the live "Create accessPackageCatalog" and "Update accessPackageCatalog" request-body pages state verbatim, in their own property description text, "The possible values are: unpublished and published" — a narrower, two-value set. This module'svalidation {}block mirrors the narrower, request-body-specific documentation rather than the broader resource-type enum definition, sinceunknownFutureValueis a server-side evolvable-enum sentinel meant for values Graph may return in the future, not something a caller meaningfully chooses to send on a write. This is a judgment call, not a Microsoft Learn-confirmed restriction — a future maintainer who finds Graph accepting a literal"unknownFutureValue"write (or documents a fourth real state value) should revisit this validation.stateandis_externally_visibleboth carry an explicit default that this module sets regardless of Graph's own behavior, per this suite's "make the empty call produce the safe resource" convention:statedefaults to"unpublished"because Graph's own Create reference documents no fallback if the property is omitted (genuinely undocumented, not merely unresearched);is_externally_visibledefaults tofalse, which happens to also match Graph's own confirmed default per the Microsoft Graph PowerShell SDK'sNew-MgEntitlementManagementCatalogcmdlet reference, but this module still sets it explicitly rather than relying on that implicit behavior, since this suite's convention is never to rely on an implicit default for a security-relevant field even when the implicit default happens to already be safe.- This module has no keystone-owned
for_eachchildren — it's a genuinely single-resource standalone module. The example library's scaling pattern (Example 9) is a caller-sidefor_eachover the module call itself, keyed by a stable map key, nevercount. - No property on this entity is documented as immutable/force-new.
displayName,description,state, andisExternallyVisibleare all confirmed mutable via ordinaryPATCHon the live "Update accessPackageCatalog" reference — there is noterraform_data+lifecycle.replace_triggered_bymechanism in this module (unlike, e.g.,named-location's@odata.typediscriminant oraccess-package's force-newcatalogreference), because there is nothing in this entity's own schema that needs one. - The Delete reference page documents no precondition about catalog contents. A direct fetch of the live
"Delete accessPackageCatalog" reference during this authoring session confirmed the documented request
takes no request body and returns a 200-series/204 No Content response with no stated "catalog must be
empty" constraint — a genuine, confirmed asymmetry with the sibling
access-packagemodule's explicitly documented "cannot delete with assignments" rule. Treat this as Graph's undocumented behavior, not an enforced API rule — see 🔍 Troubleshooting for the recommended operational precaution.
| Concern | Secure default in this module | Opt-out (caller must be explicit) |
|---|---|---|
| Catalog publication state | state defaults to "unpublished" — an explicit hardening choice layered on top of Graph's own undocumented (not merely absent) Create-time behavior |
Caller sets state = "published" explicitly |
| External visibility | is_externally_visible defaults to false — matches Graph's own confirmed default, set explicitly at the Terraform level regardless per this suite's convention against relying on implicit defaults for security-relevant fields |
Caller sets is_externally_visible = true explicitly |
Catalog classification (catalogType) |
Never caller-settable at all — system-set by Graph to userManaged, surfaced only as a computed output |
N/A — this is a hard rule, not a toggle (see 🧠 Architecture Notes) |
| Sensitive outputs | No output is ever derived from a credential-bearing property (none exists on this entity) | N/A — not applicable to this entity |
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/tf-mod-msgraph-access-package-catalog.git?ref=v1.0.0"terraform validate and terraform fmt -check prove the offline contract: display_name is non-empty,
state is restricted to "unpublished"/"published", and is_externally_visible is a well-formed bool —
all before any Graph call is made. Both were run against this module's root and its examples/basic call
during authoring; both passed cleanly with no diff.
What this cannot catch, even at plan: msgraph_resource.this's body argument is a generic map from
the provider's own perspective — the provider does not know what a Graph accessPackageCatalog is.
Concretely, in this module:
- A syntactically valid
descriptionstring that happens to be nonsensical or empty-looking (e.g. a single space) passesplancleanly — this module enforces no minimum content ondescriptionbeyond its type. - Whether the calling principal actually holds
EntitlementManagement.ReadWrite.All(or an equivalent Entra role) is invisible toplan— an under-permissioned principal'sapplyfails only at the Graph API call itself, with a 403. - Whether deleting a non-empty catalog actually succeeds or is silently blocked is not something
plan(or this module) can observe — the Delete reference page documents no precondition either way (see 🧠 Architecture Notes and 🔍 Troubleshooting). - Whether a tenant actually holds the Entra ID Governance license this entity requires is invisible to
plan— an unlicensed tenant'sapplyfails only at the Graph API call itself.
This module's typed inputs and validation {} blocks are the only thing standing between the caller and a
Graph 400 error at apply time for anything beyond these categories.
$ terraform output
catalog_type = "userManaged"
display_name = "Sales Partner Resources"
id = "b1bf99ed-99ed-b1bf-ed99-bfb1ed99bfb1"
state = "unpublished"
No credential value is ever present in terraform output, terraform show, or any other output surface of
this module — no property on the accessPackageCatalog resource type in scope here carries one.
| Symptom | Cause | Fix |
|---|---|---|
plan/apply fails with "var.state must be one of: "unpublished", "published"..." |
state was set to a value outside this module's validated set (e.g. wrong casing, or the unknownFutureValue sentinel) |
Use exactly "unpublished" or "published" (see 🧠 Architecture Notes for why unknownFutureValue is deliberately not accepted) |
plan/apply fails with "var.display_name must be a non-empty string..." |
display_name was omitted or set to "" |
Supply a non-empty, human-readable catalog name |
apply fails at create with a 403 Forbidden |
The calling principal lacks EntitlementManagement.ReadWrite.All (or an equivalent least-privileged Entra role: Catalog creator) |
Grant the application permission with admin consent, or assign the principal the Catalog creator Entra entitlement management role |
apply fails at read with a 403 Forbidden during drift detection |
The calling principal lacks EntitlementManagement.Read.All (or EntitlementManagement.ReadWrite.All) |
Grant the read-side application permission with admin consent |
apply fails at create/update referencing a licensing restriction |
Tenant lacks Microsoft Entra ID Governance (or the relevant Entra ID P2 capability) | Confirm the tenant's license SKU before applying; entitlement management catalogs require Entra ID Governance |
| Deleting a catalog appears to succeed, but downstream access packages referencing it later error | Graph's Delete reference page documents no "catalog must be empty" precondition — the delete may succeed even with packages still present, unlike the sibling access-package module's documented "cannot delete with assignments" rule |
Empty the catalog of access packages before deleting it, as safe operational practice (not a confirmed API guarantee) — see 🧠 Architecture Notes |
A newly created catalog's id isn't yet resolvable by a dependent read immediately after apply |
Some Entra entitlement management entities exhibit brief eventual-consistency delay after creation (a pattern documented elsewhere in this catalog, e.g. group) |
Retry the dependent read after a short delay; do not add a Terraform-side retry loop — this is an operational, not a configuration, concern |
catalog_type output is unexpectedly not "userManaged" |
Should not occur for any catalog created through this module's POST endpoint — investigate whether the id in state actually corresponds to a catalog created some other way (e.g. imported from a serviceDefault/serviceManaged built-in catalog) |
Confirm via terraform state show msgraph_resource.this and a direct Graph GET on the catalog id; this module is not designed to manage non-userManaged catalogs |
- Graph API reference —
accessPackageCatalogresource type: https://learn.microsoft.com/graph/api/resources/accesspackagecatalog?view=graph-rest-1.0 - Graph API reference — Create accessPackageCatalog (Create request-body table, permissions): https://learn.microsoft.com/graph/api/entitlementmanagement-post-catalogs?view=graph-rest-1.0
- Graph API reference — Get accessPackageCatalog (permissions table): https://learn.microsoft.com/graph/api/accesspackagecatalog-get?view=graph-rest-1.0
- Graph API reference — Update accessPackageCatalog (Update request-body table, permissions): https://learn.microsoft.com/graph/api/accesspackagecatalog-update?view=graph-rest-1.0
- Graph API reference — Delete accessPackageCatalog (permissions, no documented precondition): https://learn.microsoft.com/graph/api/accesspackagecatalog-delete?view=graph-rest-1.0
- Graph API reference —
entitlementManagementsingleton resource type: https://learn.microsoft.com/graph/api/resources/entitlementmanagement?view=graph-rest-1.0 - Microsoft Graph PowerShell SDK —
New-MgEntitlementManagementCatalog(isExternallyVisibledefault confirmation): https://learn.microsoft.com/powershell/module/microsoft.graph.identity.governance/new-mgentitlementmanagementcatalog?view=graph-powershell-1.0 - Provider docs —
microsoft/msgraph0.3.0: https://registry.terraform.io/providers/microsoft/msgraph/0.3.0/docs - Sibling module (planned —
SCOPE.mdwritten,providers.tfscaffolded, novariables.tf/main.tf/outputs.tfyet) —tf-mod-msgraph-access-package'sSCOPE.md - This module's design record — this module's embedded lightweight SCOPE.md (a standalone
SCOPE.mdfile has not been promoted out of it — see 📁 Module Structure)