Manages the create/read/update/delete lifecycle of Microsoft Graph
accessPackagerecords (identityGovernance/entitlementManagement/accessPackages) plus theirresourceRoleScopeschild collection, viamsgraph_resource.this, targetingmicrosoft/msgraph0.3.0.
- π Provisions an access package β the catalog item end users request through Entra entitlement
management β via
POST identityGovernance/entitlementManagement/accessPackages, then grants it resource access by rendering zero or moreresourceRoleScopeschildren. - π§± One keystone, one
for_eachchild.msgraph_resource.thisis the access package itself;msgraph_resource.resource_role_scopes, keyed byvar.resource_role_scopes(a caller-chosen stable string, conventionally aresourceId_roleIdcomposite perSCOPE.md), is the owned child collection. - πͺ This is the narrower "catalog item" module from a 3-way split of the original
access-packagecatalog entry: catalog lifecycle belongs toterraform-msgraph-access-package-catalog; request/approval policy belongs toterraform-msgraph-access-package-assignment-policy. This module owns exactly theaccessPackageentity and itsresourceRoleScopes, nothing more. - π
catalog_idis required on create and immutable thereafter β Microsoft Learn's Update reference documents onlydisplayName/description/isHiddenas updatable; moving a package to a different catalog is not supported in place. β οΈ resourceRoleScopeshas no Graph Update method. Same shape of gap asterraform-msgraph-app-role-assignment: this module closes it with aterraform_data+lifecycle.replace_triggered_bypair, so a changed role/scope identifier under an existing map key is a clean plan-time replacement, not an apply-time Graph rejection.- π§ Known gap, documented, not silently swallowed: onboarding a catalog resource (a group,
application, or SharePoint site) so it has roles/scopes to reference in the first place β
accessPackageResourceRequestβ is explicitly out of scope.resource_role_scopesaccepts already-known role/scope identifiers as plain strings, looked up out of band viacatalogs/{id}/resourceRolesandcatalogs/{id}/resources. See π§ Architecture Notes.
π‘ Why it matters: an access package with no
resourceRoleScopesis a real, valid Graph object β it can be created, described, and later referenced by an assignment policy β but it grants no actual resource access until at least oneresourceRoleScopesentry exists. Splitting catalog lifecycle, package lifecycle, and policy lifecycle into three modules (per this suite's explicit convention against an oversized composite) means each piece can be planned, reviewed, and applied independently, at the cost of the caller needing to wire all three together deliberately β see the mandatory end-to-end composition in π Example Library.
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!
terraform-msgraph-access-package-catalog has real, validated .tf files in this repository as of this
module's authoring (confirmed by directory listing: main.tf, variables.tf, outputs.tf,
providers.tf all present) β the upstream half of the composition shown below is a real, runnable
reference, not an illustrative placeholder. terraform-msgraph-access-package-assignment-policy currently
has only a providers.tf (confirmed by the same directory listing) β it is not yet authored as a
consumable module; the downstream edge below reflects SCOPE.md's documented Emits contract, not a
call you can make against a published module today.
flowchart LR
Graph["Microsoft Graph<br/>Entra ID Governance tenant<br/>(POST/GET/PATCH/DELETE identityGovernance/entitlementManagement/accessPackages)"]:::external
Catalog["terraform-msgraph-access-package-catalog<br/>(real, authored β upstream keystone)"]:::sibling
AP["terraform-msgraph-access-package<br/>(this module)"]:::thisModule
Policy["terraform-msgraph-access-package-assignment-policy<br/>(not yet authored β providers.tf only)"]:::sibling
Catalog -->|"catalog.id reference"| AP
AP -->|"manages accessPackage + resourceRoleScopes via"| Graph
AP -->|"accessPackage.id reference"| Policy
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.)
One keystone (msgraph_resource.this) plus one for_each child collection
(msgraph_resource.resource_role_scopes). terraform_data.resource_role_scope_replace_trigger is a
built-in, non-Graph helper resource that gives each map entry a clean plan-time replacement path (see
π§ Architecture Notes).
flowchart TD
subgraph Inputs["Module inputs"]
V1["display_name, description,<br/>is_hidden"]:::neutral
V2["catalog_id (GUID,<br/>REQUIRED, force-new)"]:::neutral
V3["resource_role_scopes map<br/>(default {}), keyed by a stable<br/>resourceId_roleId composite"]:::neutral
end
This["msgraph_resource.this<br/>url = identityGovernance/entitlementManagement/accessPackages<br/>keystone"]:::thisModule
TD["terraform_data.resource_role_scope_replace_trigger[each.key]<br/>input = each.value<br/>(built-in resource β forces replace on any field change)"]:::neutral
RRS["msgraph_resource.resource_role_scopes[each.key]<br/>for_each = var.resource_role_scopes<br/>url = accessPackages/id/resourceRoleScopes<br/>body = role, scope"]:::child
subgraph Outputs["Outputs"]
O1["id (primary)"]:::neutral
O2["display_name"]:::neutral
O3["resource_role_scopes map"]:::neutral
end
V1 --> This
V2 --> This
This -->|"msgraph_resource.this.id"| RRS
V3 -->|"for_each"| TD
V3 -->|"for_each"| RRS
TD -->|"lifecycle.replace_triggered_by"| RRS
This --> O1
This --> O2
RRS --> O3
classDef thisModule fill:#0078D4,color:#ffffff,stroke:#005A9E,stroke-width:2px
classDef child fill:#005A9E,color:#ffffff,stroke:#003D66,stroke-width:1px
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 β the accessPackage entity itself |
terraform_data.resource_role_scope_replace_trigger |
terraform_data (Terraform built-in β no provider needed) |
Per-entry force-new trigger: input is the whole map entry, so any change under an existing key replaces it, and replace_triggered_by propagates that replacement to the real Graph resource |
msgraph_resource.resource_role_scopes |
msgraph_resource |
The resourceRoleScope record itself β one per for_each entry, at url = "identityGovernance/entitlementManagement/accessPackages/{id}/resourceRoleScopes" |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
microsoft/msgraph |
0.3.0, pinned exactly (pre-1.0 provider β no ~> constraint) |
Built-in terraform_data |
Part of Terraform's own terraform.io/builtin/terraform provider β no required_providers entry, no provider {} block |
| Graph API version | v1.0 (identityGovernance/entitlementManagement/accessPackages, .../resourceRoleScopes) β no beta dependency |
| Provider block | None in this module β the caller configures provider "msgraph" {} (auth, tenant, API version) in the root module |
Schema notes that bite
catalogis required on create and immutable thereafter. Microsoft Learn's "Update accessPackage" reference documents onlydisplayName/description/isHiddenas updatable properties βcatalogis not in that list. Changingcatalog_idafter creation requires destroying and recreating the access package (and, transitively, itsresourceRoleScopeschildren and any sibling access-package-assignment-policy referencing this package'sid). Not mechanically enforced via a Terraformlifecycleblock β the same established convention asterraform-msgraph-group'smailNicknameβ so an attempted change PATCHes through and fails atapply, not atplan.resourceRoleScopeshas no Update method at all. Confirmed against a fresh Microsoft Learn pull of the liveaccessPackageResourceRoleScoperesource-type reference (Methods table: List, Create, Delete only). This module forces a plan-time delete+recreate on any field change viaterraform_data+lifecycle.replace_triggered_byβ see π§ Architecture Notes.- You cannot delete an access package that has any
accessPackageAssignment. Confirmed verbatim against Microsoft Learn's "Delete accessPackage" reference: "You cannot delete an access package if it has any accessPackageAssignment." Remove all assignments (via anaccessPackageAssignmentRequest, outside this module's scope) before aterraform destroyof this module can succeed. See π Troubleshooting. - Graph's own computed
idfor a createdresourceRoleScoperecord is"{role.id}_{scope.id}"β observed directly in Microsoft Learn's own Create examples (e.g."cde82ecb-e461-496b-98fb-4f807c7ca640_dbeb8772-9907-4e95-a28e-a8d70dbcda69"). This is a different string from this module'sfor_eachmap key (a caller-chosenresourceId_roleId-style convention perSCOPE.md) β do not conflate the two. The Graph-computed id is only known afterapply(surfaced via theresource_role_scopesoutput); the map key must be known atplantime and is therefore always caller-supplied. role_origin_system/scope_origin_systemare not validated against a closed enum. Microsoft Learn documents common values ("such as"AadGroup,AadApplication,SharePointOnline,AzureResources,CustomDataProvidedResource") β its own wording, not a closed set β so novalidation {}` restricts these to a fixed list, unlike this library's usual practice for genuinely closed Graph enums.
| Operation | Application permission |
|---|---|
| Create / Update / Delete access package (keystone) | EntitlementManagement.ReadWrite.All (least-privileged Entra role: Access package manager or Catalog owner; higher-privileged alternative: Identity Governance Administrator) |
Create / Delete resourceRoleScopes child |
EntitlementManagement.ReadWrite.All (same least-privileged roles) |
All operations require admin consent. Confirmed against the live "Create accessPackage," "Update
accessPackage," "Delete accessPackage," and "Create resourceRoleScope" Microsoft Learn permission tables
β all four agree exactly on EntitlementManagement.ReadWrite.All as the sole application permission.
- Graph API version: v1.0 β no beta dependency for either the keystone or the
resourceRoleScopeschild (confirmed the v1.0 nav path isresourceRoleScopes, not the beta-onlyaccessPackageResourceRoleScopespath, which additionally covers more resource kinds). - License/SKU: Microsoft Entra ID Governance (member users); some entitlement-management capabilities also operate under Entra ID P2 β confirm current SKU naming/bundling against Microsoft Learn before publishing, since bundling changes.
- Admin consent: required for
EntitlementManagement.ReadWrite.All.
terraform-msgraph-access-package/
βββ providers.tf # required_version >= 1.12.0; microsoft/msgraph pinned exactly at 0.3.0;
β # no provider {} block; terraform_data needs no provider entry at all
βββ variables.tf # display_name, description, is_hidden, catalog_id, resource_role_scopes
βββ main.tf # msgraph_resource.this + terraform_data + msgraph_resource.resource_role_scopes
βββ outputs.tf # id (primary), display_name, resource_role_scopes
βββ README.md # this file
βββ SCOPE.md # this module's design record β permissions, prerequisites, provider gotchas
βββ examples/
βββ basic/
βββ main.tf # smallest real call β display_name + catalog_id 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 "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
description = "Access package for outside sales representatives"
catalog_id = module.sales_catalog.id # a terraform-msgraph-access-package-catalog module's id output
}
output "access_package_id" {
value = module.sales_access_package.id
}Consumes
| Input | Type | Source module |
|---|---|---|
catalog_id |
string (Guid) | terraform-msgraph-access-package-catalog module id output |
resource_role_scopes[*].role_id / role_origin_id / role_origin_system / role_resource_id |
string | Pre-existing catalog resource role identifiers β out-of-band lookup via catalogs/{id}/resourceRoles, not sourced from any module in this catalog (see π§ Architecture Notes' documented gap) |
resource_role_scopes[*].scope_id / scope_origin_id / scope_origin_system |
string | Pre-existing catalog resource scope identifiers β out-of-band lookup via catalogs/{id}/resources, same documented gap |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Graph object id (GUID) of the created access package β primary output | terraform-msgraph-access-package-assignment-policy module (accessPackage.id reference) β not yet authored, see πΊοΈ Where this fits |
display_name |
The access package's displayName, as provided to this module |
Informational only |
resource_role_scopes |
map(string), keyed by the same map key as var.resource_role_scopes β each value is Graph's computed resourceRoleScope id ("{role.id}_{scope.id}") |
Informational only β no module in this catalog consumes this today |
1 Β· Minimal required call
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = "66584aae-98bb-48cc-9458-7bee5d2a6577"
}βΉοΈ
display_nameandcatalog_idare the only two Graph-required properties. NoresourceRoleScopesare rendered β this creates a real, valid access package that grants no resource access yet.
2 Β· Staged package, hidden from requestors
module "finance_access_package_staging" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Finance Approvers"
description = "Staged β not yet visible to requestors while resourceRoleScopes are wired up"
catalog_id = module.finance_catalog.id
is_hidden = true
}π‘ Set
is_hidden = truewhile resourceRoleScopes and an assignment policy are still being wired up, then flip it tofalse(an in-place update βisHiddenis documented as updatable) once the package is ready to be requestable.
3 Β· Adding a resourceRoleScopes entry for an onboarded AAD group (root scope)
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
resource_role_scopes = {
"9028d19c-26a9-4809-8e3f-20ff73e2d75e_member" = {
role_origin_id = "9028d19c-26a9-4809-8e3f-20ff73e2d75e" # the group's own object id
role_origin_system = "AadGroup"
scope_origin_id = "9028d19c-26a9-4809-8e3f-20ff73e2d75e"
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
}
}
β οΈ Out-of-scope prerequisite: this group must already be onboarded as anaccessPackageResourceinto the same catalog (a separateaccessPackageResourceRequest, not performed by this module) before this call succeeds β see π§ Architecture Notes.
4 Β· Adding a resourceRoleScopes entry for an application role (non-root scope)
module "internal_tools_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Internal Tools Standard User"
catalog_id = module.internal_tools_catalog.id
resource_role_scopes = {
"5f80c0c7-a180-4521-b585-18200048a0d8_cde82ecb-e461-496b-98fb-4f807c7ca640" = {
role_id = "cde82ecb-e461-496b-98fb-4f807c7ca640" # accessPackageResourceRole.id, from
# listing catalogs/{id}/resourceRoles
role_origin_id = "a29a7690-b3c4-4ed5-96c6-f640cde06fb8"
role_origin_system = "AadApplication"
role_resource_id = "5f80c0c7-a180-4521-b585-18200048a0d8" # the onboarded accessPackageResource's id
scope_id = "dbeb8772-9907-4e95-a28e-a8d70dbcda69"
scope_origin_id = "e81d7f57-0840-45e1-894b-f505c1bdcc1f"
scope_origin_system = "AadApplication"
}
}
}βΉοΈ Compare this to Example 3 β different origin systems populate a different subset of fields.
role_resource_iddisambiguates which onboarded catalog resource a role belongs to; it is optional in this module's schema because not every documented Create example populates it the same way.
5 Β· Multiple resourceRoleScopes in one call
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
resource_role_scopes = {
"sales-group_member" = {
role_origin_id = module.sales_group.id
role_origin_system = "AadGroup"
scope_origin_id = module.sales_group.id
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
"crm-app_standard-user" = {
role_id = local.crm_standard_user_role_id
role_origin_id = local.crm_app_id
role_origin_system = "AadApplication"
role_resource_id = local.crm_app_resource_id
scope_id = local.crm_app_scope_id
scope_origin_id = local.crm_app_id
scope_origin_system = "AadApplication"
}
}
}π‘ Keying by a stable, human-readable map key (never an array index) means adding or removing one
resourceRoleScopesentry never forces Terraform to re-index β and therefore never re-plans β any other entry in the map.
6 Β· Zero-ID role (role_id omitted)
module "internal_tools_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Internal Tools Standard User"
catalog_id = module.internal_tools_catalog.id
resource_role_scopes = {
"resource-abc_zero-role" = {
# role_id deliberately omitted (left null) β Microsoft Learn: "If the role returned from the list
# of the roles has a zero ID, then don't include an id in the accessPackageResourceRole."
role_origin_id = local.resource_origin_id
role_origin_system = "SharePointOnline"
scope_origin_id = local.resource_origin_id
scope_origin_system = "SharePointOnline"
scope_is_root_scope = true
}
}
}βΉοΈ
role_idisoptional(string)in this module's schema for exactly this documented case.
7 Β· Updating displayName/description/isHidden in place
# Before:
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Reps"
catalog_id = module.sales_catalog.id
}
# After:
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives" # changed β clean in-place update
description = "Outside sales representatives" # added β clean in-place update
catalog_id = module.sales_catalog.id # unchanged
}βΉοΈ
displayName,description, andisHiddenare the only three properties Microsoft Learn documents as updatable βplanshows these as a clean in-place update, not a replacement.
8 Β· Changing catalog_id (force-new gotcha)
# Before:
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
}
# After β moving the package to a different catalog:
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.regional_sales_catalog.id # changed
}
β οΈ This is not mechanically caught atplantime βcatalog_idis documented immutable, but this module does not enforce it via a Terraformlifecycleblock (same convention asterraform-msgraph-group'smailNickname).planwill show an in-place update;applywill fail against Graph with a 400. The only clean path to move a package to a different catalog isterraform destroyfollowed by a newapplywith the newcatalog_idβ see π Troubleshooting.
9 Β· Empty resourceRoleScopes β the secure-by-default no-op
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
# resource_role_scopes left at its default, {} β creates the access package with no resource access
# granted yet.
}π‘
resource_role_scopesdefaults to an empty map. The empty call is a safe no-op with respect to resource access β consistent with this suite's secure-by-default convention β while still creating a real, describable access package.
10 Β· A validation failure this module catches before any Graph call
module "bad_example" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Broken Package"
catalog_id = "66584aae-98bb-48cc-9458-7bee5d2a6577"
resource_role_scopes = {
"bad-entry" = {
role_origin_id = "" # fails the non-empty-string validation{} block
role_origin_system = "AadGroup"
scope_origin_id = "9028d19c-26a9-4809-8e3f-20ff73e2d75e"
scope_origin_system = "AadGroup"
}
}
}
β οΈ This failsterraform validate/plan, notapplyβ everyerror_messagenames the offending map key(s) directly. Compare this to π§ͺ Testing, below: shape-level mistakes like this one are caught for free; whether a syntactically valid identifier actually refers to a resource/role/scope already onboarded into the catalog is not, because that requires a live Graph lookup this module cannot perform atplantime.
11 Β· Least-privilege permissions callout
# Root module β provider auth configured with exactly what this module needs: EntitlementManagement.ReadWrite.All,
# ideally scoped down further via the Access package manager or Catalog owner Entra role rather than the
# broader Identity Governance Administrator role.
provider "msgraph" {}
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
}π See π Graph API Permissions Required β do not grant the higher-privileged
Identity Governance Administratorrole unless the pipeline's other modules genuinely require it.
12 Β· Retargeting a resourceRoleScopes entry (forced replacement, not an update)
# Before:
resource_role_scopes = {
"contractor-access" = {
role_origin_id = local.old_group_id
role_origin_system = "AadGroup"
scope_origin_id = local.old_group_id
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
}
# After β repointing the same conceptual grant at a different onboarded group:
resource_role_scopes = {
"contractor-access" = {
role_origin_id = local.new_group_id # changed
role_origin_system = "AadGroup"
scope_origin_id = local.new_group_id # changed
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
}
β οΈ Changing any field under an existing map key is a real, expected delete+recreate βplanshows this entry as-/+(replace), not a clean in-place update, because Graph has no Update method foraccessPackageResourceRoleScope. See π§ Architecture Notes for theterraform_datamechanism that makes this a clean plan-time replacement instead of an apply-time Graph error.
13 Β· Consuming outputs for reporting
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
catalog_id = module.sales_catalog.id
resource_role_scopes = {
"sales-group_member" = {
role_origin_id = module.sales_group.id
role_origin_system = "AadGroup"
scope_origin_id = module.sales_group.id
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
}
}
output "sales_access_package_report" {
value = {
package_id = module.sales_access_package.id
display_name = module.sales_access_package.display_name
resource_role_scopes = module.sales_access_package.resource_role_scopes
}
}βΉοΈ
resource_role_scopesis amap(string)of Graph-computedresourceRoleScopeids β useful for audit reporting, not itself consumed by another module in this catalog today.
14 Β· ποΈ End-to-end composition β catalog, access package, and (planned) assignment policy
terraform-msgraph-access-package-catalog has real, validated .tf files in this repository β the first
two modules below are a real, runnable chain. terraform-msgraph-access-package-assignment-policy is
not yet authored (confirmed by directory listing: only providers.tf exists) β its block is shown
commented out, illustrating the intended contract from SCOPE.md's Emits table, not a call you can
make today.
module "sales_catalog" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package-catalog.git?ref=v1.0.0"
display_name = "Sales Resources"
state = "published"
}
module "sales_access_package" {
source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package.git?ref=v1.0.0"
display_name = "Sales Representatives"
description = "Outside sales representatives"
catalog_id = module.sales_catalog.id
resource_role_scopes = {
"sales-group_member" = {
role_origin_id = module.sales_group.id # a terraform-msgraph-group module's id, onboarded into
role_origin_system = "AadGroup" # the catalog as an accessPackageResource out of band
scope_origin_id = module.sales_group.id
scope_origin_system = "AadGroup"
scope_is_root_scope = true
}
}
}
# Illustrative only β terraform-msgraph-access-package-assignment-policy is a planned catalog entry, not
# yet authored. Chaining a request/approval policy after the access package is MANDATORY in real usage
# β a package with no assignment policy can never actually be requested by an end user.
# module "sales_access_package_default_policy" {
# source = "git::https://github.com/microsoftexpert/terraform-msgraph-access-package-assignment-policy.git?ref=v1.0.0"
#
# access_package_id = module.sales_access_package.id
# display_name = "Standard sales rep request policy"
# #... requestor/approval/review settings...
# }π‘ Notice the ordering Terraform's own dependency graph enforces without any
depends_on:catalog_idcomes fromsales_catalog.id, a real output reference β Terraform will not attempt to create the access package before the catalog exists. Onceaccess-package-assignment-policyis authored, the same pattern applies one level further: itsaccess_package_idinput would come fromsales_access_package.id.
Grouped summary
| Group | Variables |
|---|---|
| Required at create | display_name, catalog_id |
| Updatable in place | description (default null), is_hidden (default false) |
| Immutable / force-new | catalog_id |
resourceRoleScopes children |
resource_role_scopes (default {}) |
Full variable reference (5 inputs)
| Variable | Type | Default | Validation | Graph property |
|---|---|---|---|---|
display_name |
string |
β (required) | Must not be empty | displayName |
description |
string |
null |
β | description |
is_hidden |
bool |
false |
β | isHidden |
catalog_id |
string |
β (required) | Must be a GUID | catalog.id β force-new |
resource_role_scopes |
map(object({ role_id = optional(string), role_origin_id = string, role_origin_system = string, role_resource_id = optional(string), scope_id = optional(string), scope_origin_id = string, scope_origin_system = string, scope_is_root_scope = optional(bool, false) })) |
{} |
role_origin_id, role_origin_system, scope_origin_id, scope_origin_system must each be non-empty for every entry (four separate validation {} blocks, each naming the offending key(s)) |
resourceRoleScopes[*].role, .scope |
Deliberately excluded from this schema (see variables.tf's header comment and SCOPE.md's "Design
decisions"):
id,createdDateTime,modifiedDateTimeβ read-only, computed by Graph.accessPackagesIncompatibleWith,incompatibleAccessPackages,incompatibleGroups,assignmentPoliciesβ separate relationships out of scope for this module (assignmentPoliciesis owned by the siblingaccess-package-assignment-policymodule).- The entire
accessPackageResource/accessPackageResourceRequestobject graph β onboarding a resource into the catalog. Firm, deliberate scope boundary, not an open question β see π§ Architecture Notes. role.displayName/role.description/scope.displayName/scope.descriptiononresource_role_scopesentries β informational metadata Graph resolves from the origin system; this module's surface is limited to genuine identifiers.- No secret-bearing property exists on either the
accessPackageoraccessPackageResourceRoleScoperesource types, so nothing was excluded on that basis.
| Output | Description | Sensitive / excluded? |
|---|---|---|
id |
Graph object id (GUID) of the created access package. Primary output. | No |
display_name |
The access package's displayName, as provided to this module |
No |
resource_role_scopes |
map(string), keyed by the same map key as resource_role_scopes β each value is the created resourceRoleScope record's Graph-computed id ("{role.id}_{scope.id}") |
No |
No output is ever derived from a credential-bearing property β this entity carries none.
- Why
terraform_data+lifecycle.replace_triggered_byexists β the one thing to understand before touchingmain.tf. Graph never implemented an Update method foraccessPackageResourceRoleScope(confirmed against the live resource-type reference's Methods table: List, Create, Delete only). Left alone, a drifted field under an unchanged map key would make the provider attempt an apply-time PATCH Graph rejects.terraform_data.resource_role_scope_replace_trigger[each.key].inputis the whole map entry, so any change to it replaces thatterraform_datainstance, andreplace_triggered_bypropagates the replacement to the correspondingmsgraph_resource.resource_role_scopesinstance β exactly the same mechanismterraform-msgraph-app-role-assignmentuses for the same category of gap. catalog_idis force-new but not mechanically enforced.msgraph_resource's genericbodyhas no per-property schema for Terraform to key a forced replacement off of β the same situation asterraform-msgraph-group'smailNickname. An attempted change PATCHes through to Graph and fails there (an apply-time 400), not atplantime. This is documented, not silently swallowed β see Example 8 and π Troubleshooting.- THE DOCUMENTED OBJECT-GRAPH GAP: catalog-resource onboarding is not covered by any module in this
catalog yet. Populating
resource_role_scopesrequires the referencedaccessPackageResource(a group, application, or SharePoint site) to already be onboarded into the same catalog as this access package β a separateaccessPackageResourceRequest, and the specific role/scope pair retrieved fromcatalogs/{id}/resourceRolesandcatalogs/{id}/resourcesfirst. This module deliberately does not build any of that β it accepts already-known role/scope identifiers as plain string inputs only. A futureaccess-package-resourcemodule would be the right place to own catalog-resource onboarding; folding it into this module would force an oversized composite per this suite's explicit convention against that. Until that module exists, every caller of this module must look uprole_origin_id/role_origin_system/scope_origin_id/scope_origin_system(and, where applicable,role_id/scope_id/role_resource_id) out of band β via Graph Explorer, PowerShell, or a portal lookup β before populatingresource_role_scopes. - You cannot delete an access package that has any
accessPackageAssignment. This is a real, documented hard constraint (Microsoft Learn's "Delete accessPackage" reference), not a Terraform or provider limitation βterraform destroyon this module will fail against Graph if any user currently holds an assignment to this package. See π Troubleshooting for the remediation path. for_eachkey stability.SCOPE.md's Design decisions direct aresourceId_roleIdcomposite naming convention forresource_role_scopesmap keys. This is a naming convention only, not enforced by Terraform β note that Graph's own computedidfor the created record is actually"{role.id}_{scope.id}"(confirmed from live Microsoft Learn examples), a related but distinct string from the caller-chosen map key, since the map key must be known atplantime and role/scope ids are frequently not (see Example 6's zero-ID role case).
| Concern | Secure default in this module | Opt-out (caller must be explicit) |
|---|---|---|
| Visibility to requestors | is_hidden defaults to false (Graph documents no explicit platform default; this is the ordinary published state, not an hardening) |
Caller sets is_hidden = true to stage a package before it's ready to be requested |
Empty resource_role_scopes |
Defaults to {} β a real, valid access package is created, but grants no resource access until at least one entry is added |
Caller supplies one or more map entries explicitly |
| Catalog-resource onboarding | Never performed by this module β a firm scope boundary, not a shortcut this module takes | N/A β a future access-package-resource module owns this; not an opt-out, an architectural boundary |
| Force-new field changes | Any change to a resource_role_scopes entry's identifiers under an existing map key is always a visible, plan-time delete+recreate β never a silent apply-time failure |
None β this reflects a genuine Graph API limitation (no Update method), not an policy choice |
| Sensitive outputs | No output is ever derived from a credential-bearing property | N/A β this entity carries no secret-bearing property at all |
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-access-package.git?ref=v1.0.0"terraform validate and terraform fmt -check prove the offline contract: display_name is non-empty,
catalog_id matches a GUID shape, and every resource_role_scopes entry has non-empty
role_origin_id/role_origin_system/scope_origin_id/scope_origin_system before any Graph call is
made β a malformed entry fails plan, never silently reaches apply.
What this cannot catch: msgraph_resource.this and msgraph_resource.resource_role_scopes's body
arguments are generic maps from the provider's own perspective β the provider does not know what an
accessPackage or accessPackageResourceRoleScope is. Concretely, in this module:
- A syntactically valid GUID
catalog_idthat does not correspond to a real, existing catalog passesvalidate/plancleanly and fails only atapply, with a Graph-side 400 or 404. - A
resource_role_scopesentry whoserole_origin_id/scope_origin_iddo not correspond to an actually-onboardedaccessPackageResourcein the same catalog cannot be checked atplantime β it requires a live read ofcatalogs/{id}/resourcesandcatalogs/{id}/resourceRoles, which this module deliberately does not perform (see π§ Architecture Notes' documented object-graph gap). - An attempted change to
catalog_idon an existing access package is shown byplanas an in-place update (this module does not force a Terraform-level replacement on it) but fails atapplywith a Graph 400, sincecatalogis not in Graph's documented Update property list (see Example 8). terraform destroyagainst an access package that still has anyaccessPackageAssignmentfails atapplywith a Graph error β this cannot be predicted atplantime, since it depends on live assignment state this module does not read.
This module's non-empty-string and GUID-shape validation {} blocks are the only thing standing between
the caller and a Graph 4xx error at apply time for anything beyond shape-level mistakes.
$ terraform output
id = "642181f0-bc17-4fc6-9ebb-ff53dbf18c2f"
display_name = "Sales Representatives"
resource_role_scopes = {
"sales-group_member" = "cde82ecb-e461-496b-98fb-4f807c7ca640_dbeb8772-9907-4e95-a28e-a8d70dbcda69"
}
| Symptom | Cause | Fix |
|---|---|---|
terraform destroy fails with a Graph error referencing accessPackageAssignment |
You cannot delete an access package that has any active assignment β a real, documented hard constraint, not a Terraform or provider defect | Remove all assignments first (via an accessPackageAssignmentRequest with requestType: adminRemove, outside this module's scope), then retry destroy |
plan shows an in-place update to catalog_id, but apply fails with a 400 |
catalog is documented immutable β this module does not force a Terraform-level replacement on it (see π§ Architecture Notes) |
Revert catalog_id, or destroy and recreate the access package with the new catalog; do not expect an in-place move |
plan shows a resource_role_scopes entry as replace (-/+) instead of update-in-place |
Expected β a field under an existing map key changed, and Graph has no Update method for accessPackageResourceRoleScope |
Confirm the new values are correct, then apply; if unintentional, revert the values back |
apply fails creating a resourceRoleScopes entry with a 400/404 |
The referenced role/scope origin identifiers don't correspond to a resource actually onboarded into this package's catalog | Confirm the resource is onboarded (catalogs/{id}/resources) and the role/scope pair is correct (catalogs/{id}/resourceRoles) before reapplying β this cannot be caught at plan time |
apply fails with a 403 |
The calling principal lacks EntitlementManagement.ReadWrite.All (or an equivalent Entra entitlement-management role), or admin consent hasn't been granted |
Confirm the permission and admin consent β see π Graph API Permissions Required |
apply is slow to reflect a just-created catalog when used as catalog_id |
Graph's Entra ID Governance write path is not always immediately read-consistent | Re-run plan/apply; do not add a fixed sleep β retry with backoff if this recurs |
- Graph API reference β
accessPackageresource type: https://learn.microsoft.com/graph/api/resources/accesspackage?view=graph-rest-1.0 - Graph API reference β create accessPackage: https://learn.microsoft.com/graph/api/entitlementmanagement-post-accesspackages?view=graph-rest-1.0
- Graph API reference β update accessPackage: https://learn.microsoft.com/graph/api/accesspackage-update?view=graph-rest-1.0
- Graph API reference β delete accessPackage: https://learn.microsoft.com/graph/api/accesspackage-delete?view=graph-rest-1.0
- Graph API reference β
accessPackageResourceRoleScoperesource type: https://learn.microsoft.com/graph/api/resources/accesspackageresourcerolescope?view=graph-rest-1.0 - Graph API reference β create resourceRoleScope: https://learn.microsoft.com/graph/api/accesspackage-post-resourcerolescopes?view=graph-rest-1.0
- Provider docs β
microsoft/msgraph0.3.0msgraph_resource: https://registry.terraform.io/providers/microsoft/msgraph/0.3.0/docs/resources/resource - Terraform language reference β the
lifecyclemeta-argument (replace_triggered_by,terraform_data): https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle - Sibling modules β
terraform-msgraph-access-package-catalog(real, authored),terraform-msgraph-access-package-assignment-policy(not yet authored) - This module's design record β
SCOPE.md