Skip to content

microsoftexpert/tf-mod-msgraph-access-package-catalog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔷 Microsoft Graph Access Package Catalog Terraform Module

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/catalogs endpoint) — the container that groups resources (groups, applications, SharePoint sites) available for inclusion in access packages — via a single msgraph_resource.this, targeting microsoft/msgraph 0.3.0.

Terraform msgraph Module Type Resources


🧩 Overview

  • 📦 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 on terraform 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-package modules (one catalog can back many packages). Its two siblings from that split — access-package (the catalog-item entity) and access-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. state defaults to "unpublished" and is_externally_visible defaults to false, 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.
  • 🚫 catalogType is never a caller-settable input. It is system-set by Graph to "userManaged" on creation via this endpoint and surfaced only as a computed catalog_type output, read back through the provider's response_export_values mechanism — the same pattern authentication-strength-policy uses for policyType.
  • 🆔 Its id output is the reference type the access-package module's planned catalog_id input 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.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits

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
Loading

Validated via the Mermaid Chart MCP before embedding (valid: true).


🧬 What this builds

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
Loading

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

✅ Provider / Versions

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

  • catalogType is 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's 201 response echoes catalogType = "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/published only, stated verbatim as "The possible values are: unpublished and published"). This module's validation {} restricts to the narrower, request-body-specific two values — see 🧠 Architecture Notes for the full rationale on this judgment call.
  • state has 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 ordinary PATCH on the live "Update accessPackageCatalog" reference.
  • The Delete reference page is silent on any "catalog must be empty" precondition — unlike the sibling access-package module'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.

🔑 Graph API Permissions Required

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.

Microsoft Graph Prerequisites

  • 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.

📁 Module Structure

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.md file.


⚙️ Quick Start

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
}

🔌 Cross-Module Contract

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

📚 Example Library

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"
}

ℹ️ description is left null, state defaults to "unpublished", and is_externally_visible defaults to false — 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 = true must be set explicitly — this module's secure default is false, 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_type is never a caller input — it's read back from Graph's create response via response_export_values and 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 fails terraform plan, not applyvar.state's validation {} 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 count means adding or removing one department from var.department_catalogs never 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.All instead.

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, and isExternallyVisible are all confirmed mutable via ordinary PATCH on 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-package is fully authored, the ordering shown here is exactly what Terraform's own dependency graph enforces without any depends_on: the package's catalog_id input takes module.partner_resources_catalog.id as 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.


📥 Inputs

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).


🧾 Outputs

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.


🧠 Architecture Notes

  • catalogType is deliberately never a caller input, even though a fresh Microsoft Learn fetch of the Update page during this authoring session confirms catalogType is technically listed as an updatable property on PATCH /identityGovernance/entitlementManagement/catalogs/{id}. This module still excludes it from variables.tf entirely, per this module's embedded SCOPE.md and the explicit authoring instruction behind it: the Create request-body table does not list it at all (every documented Create example's response echoes catalogType = "userManaged" without the caller supplying it), and re-designating an existing catalog's userManaged/serviceDefault/serviceManaged classification 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 computed catalog_type output, read back via response_export_values — the same mechanism tf-mod-msgraph-authentication-strength-policy uses for its own read-only policyType/requirementsSatisfied properties.
  • state's legal-value set is deliberately narrower than the general resource-type page's documented enum. The accessPackageCatalogState type on the general accessPackageCatalog resource-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's validation {} block mirrors the narrower, request-body-specific documentation rather than the broader resource-type enum definition, since unknownFutureValue is 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.
  • state and is_externally_visible both 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: state defaults to "unpublished" because Graph's own Create reference documents no fallback if the property is omitted (genuinely undocumented, not merely unresearched); is_externally_visible defaults to false, which happens to also match Graph's own confirmed default per the Microsoft Graph PowerShell SDK's New-MgEntitlementManagementCatalog cmdlet 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_each children — it's a genuinely single-resource standalone module. The example library's scaling pattern (Example 9) is a caller-side for_each over the module call itself, keyed by a stable map key, never count.
  • No property on this entity is documented as immutable/force-new. displayName, description, state, and isExternallyVisible are all confirmed mutable via ordinary PATCH on the live "Update accessPackageCatalog" reference — there is no terraform_data + lifecycle.replace_triggered_by mechanism in this module (unlike, e.g., named-location's @odata.type discriminant or access-package's force-new catalog reference), 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-package module'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.

🧱 Design Principles

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

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin 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"

🧪 Testing

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 description string that happens to be nonsensical or empty-looking (e.g. a single space) passes plan cleanly — this module enforces no minimum content on description beyond its type.
  • Whether the calling principal actually holds EntitlementManagement.ReadWrite.All (or an equivalent Entra role) is invisible to plan — an under-permissioned principal's apply fails 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's apply fails 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.


💬 Example Output

$ 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.


🔍 Troubleshooting

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

🔗 Related Docs

Packages

 
 
 

Contributors

Languages