Most tenants treat identity governance as a portal you visit during an audit. That is how you end up with a contractor who still holds Owner on three production subscriptions, a security group whose membership nobody can explain, and 400 guest accounts that last signed in 18 months ago. Access creep is not a one-time cleanup problem — it is a rate problem. Grants happen continuously through tickets and group adds; removal happens almost never. Governance at scale means building the machinery that revokes at the same rate access is granted: entitlement management (access packages) for time-bound self-service, lifecycle workflows for joiner-mover-leaver automation, and access reviews for periodic re-attestation. This guide wires all three together with Microsoft Graph, ties them back to Privileged Identity Management (PIM) and audit evidence, and closes with a worked joiner-mover-leaver design you can adapt.
The three features are complementary, not interchangeable, and confusing them is the most common design error. Entitlement management governs requested access — the analyst who needs the BI workspace fills a form, a manager approves, and the grant self-destructs in 180 days. Lifecycle workflows govern event-driven access — the joiner gets default groups on day one, the leaver loses everything the moment employeeLeaveDateTime passes, no ticket required. Access reviews govern residual access — the mover who changed teams but kept an old group, or the manual grant made outside any package, gets re-attested on a schedule with a deny-by-default outcome. Miss any one of the three and a whole class of access decay slips through.
Everything here requires Microsoft Entra ID Governance (or the broader Entra Suite) licensing for the principals being governed — entitlement management, lifecycle workflows, and recurring reviews are not in the base P2 SKU. Get licensing settled before you design, because the feature you scope to 9,000 employees is the feature you must license for 9,000 employees. By the end you will be able to design catalogs and access packages with real policies, stand up JML workflows with the documented built-in tasks, schedule enforcing access reviews of every object type, model separation of duties as a preventive gate, integrate PIM so privilege is time-boxed twice over, and prove all of it ran from AuditLogs rather than assuming it from config state.
What problem this solves
The pain is quantifiable and it always shows up in the same three places: the audit finding, the incident, and the offboarding gap.
The audit finding is a SOX or ISO 27001 auditor asking “who has access to the payment system, who approved it, when was it last reviewed, and prove access is removed when someone leaves.” If your answer is a spreadsheet a human maintains, you fail — a manually maintained list is a detective control at best and stale the moment it is exported. Auditors increasingly demand preventive controls: not “we caught the toxic combination in a quarterly review” but “the toxic combination was structurally impossible to grant.” Entitlement management’s separation-of-duties gate is exactly that preventive control, and access reviews are the recurring re-attestation on top of it.
The incident is a compromised or malicious account with standing privilege. A contractor account with permanent Owner, phished on a Tuesday, is a breach on Tuesday. The mitigation is not detection — it is not having standing privilege to steal in the first place. Governance delivers access as PIM-eligible rather than active, so a compromised account that has not activated holds nothing, and even an activated session is time-boxed to hours.
The offboarding gap is the leaver who departs Friday and still has VPN, email, and three SaaS seats on Monday. Manual offboarding via a ticket queue is slow, error-prone, and invisible to audit. Lifecycle workflows fire on the HR-driven leave date and de-provision in real time — disable the account, strip groups and licenses, revoke refresh tokens — with a run history you can point an auditor at.
Who hits this: every regulated organization (finance, healthcare, government), every company past a few hundred employees where central IT cannot personally know who should have what, and every tenant with a large guest population from partner collaboration. The larger the tenant, the more the rate mismatch between granting and revoking dominates, and the more the machinery matters.
To frame the whole field, here is the failure-mode map — every governance gap, what it looks like in production, and the primary control that closes it:
| Gap | What it looks like in production | Primary control | Reinforced by |
|---|---|---|---|
| Access creep | Permissions accumulate through tickets/group adds, never shrink | Expiring access packages (expiration) |
Recurring access reviews |
| Orphaned access | Leaver/mover keeps old entitlements after departure or transfer | Lifecycle workflows on JML triggers | Mover reviews; SoD |
| Standing privilege | Permanent admin on high-value scopes | PIM eligible assignments | Reviews over eligible assignments |
| Toxic combination | One person can both create and approve a payment | Separation-of-duties (incompatible packages) | SoD review over the sensitive package |
| Guest decay | Partner guests never offboarded, invisible to HR | Externally visible catalog auto-removal | Inactive-guest reviews; connected orgs |
| Audit blindness | Cannot prove who approved what, when, or that it was reviewed | Catalog policies + review decision logs | AuditLogs / decision exports |
| Ticket bottleneck | Every access request routes through central IT | Delegated catalogs + self-service packages | Resource-owner approval |
The mental shift: stop asking “who has access?” Start asking “who has access, granted by whom, justified how, expiring when, and last reviewed on what date?” If you cannot answer all five from a query, you do not have governance — you have a list.
Learning objectives
By the end of this article you can:
- Distinguish entitlement management, access reviews, and lifecycle workflows by the kind of access decay each one closes, and combine all three into a coherent program.
- Design catalogs, access packages, resource roles, and assignment policies — including delegation to resource owners, multi-stage approval, verified-ID requirements, and time-bound expiration — with real Microsoft Graph configuration.
- Enforce separation of duties as a preventive request-time gate using incompatible access packages and groups, in both directions.
- Configure auto-assignment policies so access follows a user attribute rule with no request at all, and know when that beats a request policy.
- Schedule access reviews of groups, applications, directory/PIM roles, and access packages — choosing reviewers, recurrence, decision helpers, non-response defaults, and auto-apply — so a review enforces rather than merely reports.
- Build joiner, mover, and leaver lifecycle workflows with the documented built-in
taskDefinitionIdtasks, correct triggers and scope, and custom task extensions for systems Graph cannot reach. - Integrate governance with PIM so privileged access is delivered as expiring eligibility on a PIM-enabled group, and reviewed as eligibility rather than active membership.
- Onboard external partners with connected organizations so B2B guests flow through governed catalogs, and prove the whole machine ran from
AuditLogsand per-cycle decision exports.
Prerequisites & where this fits
You should already understand core Entra ID: users, groups (security and Microsoft 365), enterprise applications and app roles, directory roles, and the difference between P1 and P2. You should be comfortable with Microsoft Graph PowerShell (Connect-MgGraph, the Microsoft.Graph.Identity.Governance module) and reading JSON/@odata.type payloads, because the portal cannot express everything governance can and IaC-style repeatability lives in Graph. Familiarity with PIM for roles and groups is assumed at the level of “eligible vs active, activation with justification” — this article uses PIM but does not re-teach it.
This sits at the top of the identity governance stack. It assumes the primitives from Privileged Identity Management and PAM Architecture: Just-in-Time Access at Scale (this article delivers governed access into PIM), and it is the operational sibling of Zero Trust on Microsoft Entra: Conditional Access + PIM, Step by Step — governance decides what a principal may hold; Conditional Access decides under what conditions they may use it. For external identities it builds directly on Entra External ID B2B and Cross-Tenant Access: Guest Collaboration, Direct Connect, and Trust Settings Done Right. Governance is one pillar of the broader Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars.
A quick map of who owns and confirms each control, so you route work to the right team:
| Layer | What lives here | Who usually owns it | Governance control it drives |
|---|---|---|---|
| HR / HCM source | employeeHireDate, employeeLeaveDateTime, department, manager |
HR + Entra provisioning | Lifecycle workflow triggers and scope |
| Catalog / resource owner | Groups, apps, sites in a business domain | The business domain team | Access package design, delegated approval |
| Central IAM team | Governance config, licensing, connected orgs | Identity/security team | Catalogs, SoD, review schedules, PIM |
| Resource / app owner | An individual app or privileged group | The service owner | Approver of requests; reviewer of reviews |
| Compliance / audit | Evidence, retention, control attestation | GRC team | Decision-log exports, AuditLogs queries |
| Security operations | Detection of what governance failed to prevent | SecOps | Backstop KQL over de-provisioning events |
Before any config, install the module and connect with the least-privilege scopes governance actually needs — four scopes cover almost everything below:
Install-Module Microsoft.Graph.Identity.Governance -Scope CurrentUser
Connect-MgGraph -Scopes @(
"EntitlementManagement.ReadWrite.All", # catalogs, packages, policies, assignments
"AccessReview.ReadWrite.All", # access review definitions and decisions
"LifecycleWorkflows.ReadWrite.All", # JML workflows, tasks, runs
"RoleManagement.ReadWrite.Directory" # PIM eligible role/group assignments
)
Core concepts
Six mental models make every later section obvious. Read these once; the rest is application.
Entitlement management is a hierarchy: catalog → resource → resource role → access package → policy. A catalog is a container of governed resources plus a delegation boundary — a resource owner manages their catalog without tenant-wide admin. A resource (a group, an app, a SharePoint site) is added to the catalog. Each resource exposes resource roles (group membership, an app role like “Reader”, a site permission level). An access package bundles specific resource roles into one requestable unit. A policy on the package defines who can request it, who approves, and how long the grant lasts. The policy is where governance lives; the package without a policy is inert.
Access packages come in two flavours: request policies and auto-assignment policies. A request policy is self-service — a user (or a request on their behalf) asks, an approver decides, and the grant is time-bound. An auto-assignment policy has no request at all — it is a membership rule (department -eq 'Finance') and every matching user is automatically granted the package’s roles, losing them when they no longer match. Request policies fit discretionary access; auto-assignment fits birthright access that should track an attribute.
Access reviews are a scope, reviewers, and settings. An accessReviewScheduleDefinition reviews a scope (a group’s members, an app’s assigned users, a role’s members, an access package’s assignments), assigns reviewers (owners, managers, the users themselves, or named people), and applies settings (recurrence, duration, decision helpers, and — critically — what happens on non-response). The two settings that turn a review from a survey into a control are autoApplyDecisionsEnabled and defaultDecision; without them, “remove” decisions rot in a report nobody actions.
Lifecycle workflows are a trigger, an execution scope, and ordered tasks. A workflow has a trigger (a time offset relative to an attribute like employeeHireDate or employeeLeaveDateTime, or an on-demand run), an execution scope (a rule selecting which users it applies to), and an ordered list of tasks referenced by stable taskDefinitionId GUIDs. Categories are joiner, mover, and leaver; the same task (e.g. “Add user to groups”) can appear in multiple categories.
PIM is orthogonal and stacks on top. Reviews and packages govern which roles a person can hold; PIM governs when they hold them. The clean pattern is that governance makes someone a member of a PIM-enabled group, and that group is eligible (not active) for a privileged role. Governance controls group membership; PIM controls activation. Two expirations then stack — eligibility (governed, months) and activation (PIM, hours).
Connected organizations extend all of this to partners. A connected organization is a registered external tenant (or domain) whose users may be invited into your catalogs. It lets an access package’s requestor scope include “users from partner X’s tenant” without opening self-service to the entire internet, and it makes the catalog the lifecycle boundary for those guests.
The vocabulary in one table
Pin down every moving part before the deep sections. The glossary repeats these for lookup; this is the mental model side by side:
| Concept | One-line definition | Where it lives | Why it matters |
|---|---|---|---|
| Catalog | Container of governed resources + delegation boundary | Entitlement management | Splits ownership; kills the central-IT bottleneck |
| Resource | A group/app/site added to a catalog | In a catalog | The thing access is granted to |
| Resource role | A specific role on a resource (member, app role, permission level) | On a catalog resource | The granularity of a grant |
| Access package | A bundle of resource roles that is requestable | In a catalog | The unit a user requests or is auto-assigned |
| Assignment policy | Who can request/get it, approval, expiry | On an access package | Where governance actually lives |
| Request policy | Policy with self-service request + approval | On an access package | Discretionary, time-bound access |
| Auto-assignment policy | Attribute-rule policy, no request | On an access package | Birthright access that tracks an attribute |
| Separation of duties | Incompatible package/group at request time | On an access package | Preventive toxic-combination gate |
| Access review | Scheduled re-attestation of a scope | Identity Governance | Removes residual/creeping access |
| Decision helper | Sign-in-based approve/deny recommendation | Review setting | Scales reviewer judgement |
| Lifecycle workflow | Trigger + scope + tasks for JML | Identity Governance | Event-driven provision/deprovision |
| Task definition | A built-in workflow action (GUID) | Workflow task | The verb: disable, remove groups, etc. |
| Custom task extension | A workflow task that calls a Logic App | Workflow task | Reach systems Graph cannot |
| Connected organization | Registered external tenant/domain | Entitlement management | Governed B2B onboarding |
| PIM-enabled group | Group whose membership can be eligible/active | PIM for Groups | The bridge from governance to JIT privilege |
Which control for which decay — the routing table
The single most useful decision you make is which of the three features owns a given access problem. Get this wrong and you build the machine but point it at the wrong door:
| Access problem | Owning control | Why not the others |
|---|---|---|
| “Analyst needs BI workspace for a project” | Access package (request policy) | Discretionary, needs approval + expiry — not birthright, not event-driven |
| “All Finance staff get the Finance apps” | Access package (auto-assignment) | Tracks an attribute; a request form would be pure friction |
| “New hire needs default groups on day one” | Lifecycle workflow (joiner) | Fires on employeeHireDate; no human request should gate day-one access |
| “Leaver must lose everything on departure” | Lifecycle workflow (leaver) | Fires on employeeLeaveDateTime; reviews are too slow, packages don’t cover manual grants |
| “Mover changed teams, has stale access” | Lifecycle workflow (mover) + review | Workflow strips known groups; review catches everything else |
| “Prove admin group still needs its members” | Access review of the group | Periodic re-attestation is exactly a review’s job |
| “One person can create and approve payments” | Separation of duties on the package | Preventive gate; a review only catches it after the fact |
| “Guest hasn’t signed in for 90 days” | Inactive-guest access review | No employeeLeaveDateTime exists, so no workflow fires |
| “Owner role held permanently” | PIM eligibility + review of eligibility | Governance delivers eligibility; PIM time-boxes activation |
Design catalogs, access packages, and policies
Entitlement management’s hierarchy exists for one reason: to let a business domain own its own access without central IT in the loop for every request. The biggest design error is one giant catalog owned by central IT — that recreates the ticket bottleneck you are trying to kill. Build catalogs per business domain (Finance, Engineering, Data Platform) and delegate ownership to those teams.
Catalogs and delegation
A catalog carries a delegation boundary. Users granted the Catalog owner role manage resources and packages inside it; Catalog reader can view. Critically, adding a directory role as a resource, or governing privileged resources, may require the actor to hold a matching directory role — you cannot delegate away more than you hold.
# Catalog scoped to a business domain, delegable to its owners
$catalog = New-MgEntitlementManagementCatalog -BodyParameter @{
displayName = "Data Platform"
description = "Governed access for the data platform domain"
isExternallyVisible = $false # internal only; flip to $true for B2B sharing via connected orgs
}
# Add a Microsoft Entra group as a governed resource in the catalog
New-MgEntitlementManagementResourceRequest -BodyParameter @{
requestType = "AdminAdd"
catalog = @{ id = $catalog.Id }
resource = @{
originId = "f3f9a4d2-0000-0000-0000-000000000001" # group objectId
originSystem = "AadGroup"
}
}
The catalog design choices and their consequences:
| Catalog setting | Values | Default | When to change | Trade-off / gotcha |
|---|---|---|---|---|
isExternallyVisible |
true / false |
false |
Enable when guests from connected orgs must request packages | External visibility exposes the catalog to configured external users only, not the world |
| Catalog scope | One per business domain | (your design) | Split when a team should self-manage | Too few = bottleneck; too many = sprawl and duplicated resources |
| Delegated roles | Catalog owner / reader | None (central) | Delegate to domain owners early | Owner can add resources they themselves can administer — not a privilege-escalation bypass |
| Resource types | Group, app, SharePoint site | (added per resource) | Add the resources the domain grants | Directory roles as resources need matching privilege to add |
| Resource origin | AadGroup, AadApplication, SharePointOnline |
— | Match the resource kind | A resource lives in exactly one catalog at a time |
Resources and resource roles
Once a resource is in the catalog, you attach one or more of its resource roles to an access package. For a group the role is membership; for an app it is a specific app role (e.g. “Reports.Read”); for a SharePoint site it is a permission level. This is the granularity control — a package can grant “member of group X and the Reader app role on app Y and Contribute on site Z” as one unit.
| Resource type | originSystem |
Resource roles available | Typical use |
|---|---|---|---|
| Security / M365 group | AadGroup |
Member (and owner, via role) | Group-based access, Teams, licensing |
| Enterprise application | AadApplication |
The app’s defined app roles | SaaS/line-of-business app entitlements |
| SharePoint Online site | SharePointOnline |
Site permission levels | Document/site access |
Access packages and assignment policies
Create the package, then bind a policy. The policy is where expiration, requestApprovalSettings, and requestorSettings live — these are not optional decorations, they are the control.
$ap = New-MgEntitlementManagementAccessPackage -BodyParameter @{
displayName = "Data Platform - Analyst (read)"
description = "Read access to the data lake and BI workspace"
catalog = @{ id = $catalog.Id }
}
New-MgEntitlementManagementAssignmentPolicy -BodyParameter @{
displayName = "Analyst - 180 day, manager approval"
accessPackage = @{ id = $ap.Id }
# Who may request: all members (scope can be narrowed to specific groups or connected orgs)
requestorSettings = @{
enableTargetsToSelfAddAccess = $true
enableTargetsToSelfRemoveAccess = $true
allowCustomAssignmentSchedule = $true
onBehalfRequestorsAllowed = $false
}
# Approval: single-stage, the requestor's manager, justification required, auto-deny after 7 days
requestApprovalSettings = @{
isApprovalRequiredForAdd = $true
stages = @(
@{
durationBeforeAutomaticDenial = "P7D"
isApproverJustificationRequired = $true
primaryApprovers = @(
@{ "@odata.type" = "#microsoft.graph.requestorManager"; managerLevel = 1 }
)
}
)
}
# Time-bound: access expires, forcing re-request
expiration = @{ type = "afterDuration"; duration = "P180D" }
}
The expiration block is the antidote to access creep. With afterDuration set to P180D, every grant self-destructs in 180 days; if the analyst still needs it, they re-request and a manager re-approves. No standing analyst access exists by default. The expiration options:
expiration.type |
Meaning | Extra fields | Use when |
|---|---|---|---|
afterDuration |
Grant lasts a fixed span, then expires | duration (ISO 8601, e.g. P180D) |
Most discretionary access — the default choice |
afterDateTime |
Grant expires on a fixed date | endDateTime |
Project-bound access with a known end |
noExpiration |
Grant never expires by itself | none | Birthright access reviewed by another control; avoid for privilege |
The requestor-settings knobs that shape self-service:
| Setting | Values | Effect | Gotcha |
|---|---|---|---|
enableTargetsToSelfAddAccess |
true/false |
Whether users may request for themselves | If false, only admins/sponsors add — not self-service |
enableTargetsToSelfRemoveAccess |
true/false |
Users can drop their own access early | Good hygiene; lets users self-reduce |
allowCustomAssignmentSchedule |
true/false |
Requestor picks start/end within limits | Bounded by the policy’s expiration |
onBehalfRequestorsAllowed |
true/false |
Managers/sponsors request for others | Needed for delegated onboarding |
Scope (allowedRequestors) |
All members / specific groups / connected orgs / all external | Who is in the requestor audience | Narrow this for sensitive packages |
Approvals, separation of duties, and expiration
Two policy capabilities turn a request form into a control: multi-stage approval and separation of duties.
Multi-stage approval
A single stages object is one approval hop. Add a second object to the array to chain independent approvers — use it when the resource owner and the requestor’s manager must both sign off, or when a security review follows a business approval. Each stage has its own timeout, its own approvers, and optional escalation.
# Two-stage approval: manager first, then the resource owner, with escalation on stage 2
requestApprovalSettings = @{
isApprovalRequiredForAdd = $true
stages = @(
@{ # Stage 1 — requestor's manager
durationBeforeAutomaticDenial = "P3D"
isApproverJustificationRequired = $true
primaryApprovers = @(
@{ "@odata.type" = "#microsoft.graph.requestorManager"; managerLevel = 1 }
)
}
@{ # Stage 2 — named resource owner, escalate to a backup if idle
durationBeforeAutomaticDenial = "P4D"
isApproverJustificationRequired = $true
isEscalationEnabled = $true
escalationTimeInMinutes = 1440
primaryApprovers = @(
@{ "@odata.type" = "#microsoft.graph.singleUser"; userId = "1111...owner" }
)
escalationApprovers = @(
@{ "@odata.type" = "#microsoft.graph.singleUser"; userId = "2222...backup" }
)
}
)
}
The approver types and when each fits:
Approver @odata.type |
Who approves | Extra field | Best for |
|---|---|---|---|
requestorManager |
The requestor’s manager | managerLevel (1 = direct) |
Default first stage; business accountability |
singleUser |
A named person | userId |
A specific resource owner |
groupMembers |
Any member of a group | groupId |
An approval rota / on-call queue |
connectedOrganizationMembers |
Sponsors of a connected org | connectedOrganizationId |
Approving external requestors |
internalSponsors / externalSponsors |
Designated sponsors | — | Sponsor-based B2B approval |
Approval-stage settings that matter beyond who approves:
| Setting | Effect | Typical value | Note |
|---|---|---|---|
durationBeforeAutomaticDenial |
Timeout → auto-deny if no decision | P3D–P7D |
Keeps requests from stalling forever |
isApproverJustificationRequired |
Approver must type a reason | true for privilege |
The reason becomes audit evidence |
isEscalationEnabled + escalationTimeInMinutes |
Route to backup after idle time | true, 1440 (1 day) |
Prevents a single absent approver blocking |
isRequestorJustificationRequired (requestor side) |
Requestor must justify | true |
Pairs with approver justification |
Separation of duties (SoD)
Separation of duties blocks toxic combinations at request time, before the grant exists. If holding both “Vendor Onboarding” and “Payment Approval” enables fraud, mark them incompatible: a user assigned to one cannot even request the other. This is far stronger than a detective control in a SIEM — the conflicting access never gets granted, so there is no window of exposure to detect. The difference between a guardrail and an alarm.
# Mark another access package as incompatible with this one (SoD)
New-MgEntitlementManagementAccessPackageIncompatibleAccessPackageByRef `
-AccessPackageId $paymentApprovalPkgId `
-BodyParameter @{
"@odata.id" = "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/accessPackages/$vendorOnboardingPkgId"
}
# You can also make membership of a security group incompatible with a package
New-MgEntitlementManagementAccessPackageIncompatibleGroupByRef `
-AccessPackageId $paymentApprovalPkgId `
-BodyParameter @{
"@odata.id" = "https://graph.microsoft.com/v1.0/groups/$contractorsGroupId"
}
Two subtleties decide whether SoD actually holds:
- Reciprocity. Marking A incompatible with B blocks requesting B while holding A. To block the reverse (requesting A while holding B) you also add the mirror reference on the other package. Configure both directions or the gate is one-way.
- Existing holders. SoD is preventive — it blocks new requests. Users who already hold both packages are not auto-removed; resolve them with a one-time targeted review, not bulk deletion, so you do not strip access someone legitimately needs.
| SoD mechanism | Blocks | Set on | Note |
|---|---|---|---|
| Incompatible access package | Requesting pkg B while holding pkg A | Each package (both directions) | The core toxic-pair control |
| Incompatible group | Requesting the package while in group G | The package | Useful when the conflict is a group, e.g. “Contractors” |
| Override (grace) | Temporarily allow a blocked request | Request-level | For exceptional, documented cases only |
Auto-assignment: access that follows an attribute
Some access is birthright — every Finance employee should have the Finance apps, full stop, with no request form. That is an auto-assignment policy: instead of requestorSettings, it carries a specificAllowedTargets membership rule, and every user matching the rule is granted the package’s roles automatically, losing them when they stop matching. There is no approval because there is no request — the rule is the approval.
New-MgEntitlementManagementAssignmentPolicy -BodyParameter @{
displayName = "Finance staff - birthright apps (auto)"
accessPackage = @{ id = $financeAppsPkgId }
# Auto-assignment: a filter over user attributes, no request/approval
specificAllowedTargets = @(
@{
"@odata.type" = "#microsoft.graph.attributeRuleMembers"
membershipRule = "(user.department -eq `"Finance`") and (user.employeeType -eq `"Employee`")"
}
)
automaticRequestSettings = @{
requestAccessForAllowedTargets = $true
removeAccessWhenTargetLeavesAllowedTargets = $true # de-provision when the rule stops matching
gracePeriodBeforeAccessRemoval = "P7D" # keep access N days after they fall out
}
expiration = @{ type = "noExpiration" } # governed by the rule, not a timer
}
Request policy vs auto-assignment, decided by the nature of the access:
| Dimension | Request policy | Auto-assignment policy |
|---|---|---|
| Trigger | User (or on-behalf) requests | Matches an attribute rule |
| Approval | One or more stages | None — the rule is the gate |
| Expiration | Timer (afterDuration/afterDateTime) |
noExpiration; removed when rule stops matching |
| Best for | Discretionary, project, elevated access | Birthright/role-based access that tracks HR data |
| Removal | On expiry or self-remove | Automatic on rule mismatch (+ grace period) |
| Audit story | “Requested, approved by X, expires Y” | “Held because attribute matched on date Z” |
| Data dependency | Low | High — only as good as the attribute source |
The removeAccessWhenTargetLeavesAllowedTargets flag is what makes auto-assignment a governance control rather than a one-way grant: when the mover’s department changes away from Finance, the package (and its roles) are removed after the grace period — an automatic, attribute-driven de-provision that complements the mover lifecycle workflow.
Automate joiner-mover-leaver with lifecycle workflows
Access packages handle requested and attribute-driven access. Lifecycle workflows handle event-driven access tied to employment milestones — the joiner who needs default groups on day one, and far more importantly, the leaver who must lose everything the moment they depart.
A workflow has a trigger (a time offset relative to an attribute like employeeHireDate or employeeLeaveDateTime), an execution scope (a rule selecting which users it applies to), and an ordered list of tasks referenced by stable taskDefinitionId GUIDs. The attribute values must actually be populated — usually by HR-driven inbound provisioning (Workday, SuccessFactors) or Entra Connect — or the workflow has nothing to fire on.
The leaver workflow — real-time offboard
The highest-value workflow. It fires on the employee leave date and de-provisions cleanly: disable, strip groups and licenses, revoke tokens. These GUIDs are the documented built-in task definitions.
New-MgIdentityGovernanceLifecycleWorkflow -BodyParameter @{
displayName = "Leaver - real-time offboard"
description = "Disable, strip groups/licenses, revoke tokens on departure"
category = "leaver"
isEnabled = $true
executionConditions = @{
"@odata.type" = "#microsoft.graph.identityGovernance.triggerAndScopeBasedConditions"
scope = @{
"@odata.type" = "#microsoft.graph.identityGovernance.ruleBasedSubjectSet"
rule = "(department -eq 'Engineering')"
}
trigger = @{
"@odata.type" = "#microsoft.graph.identityGovernance.timeBasedAttributeTrigger"
timeBasedAttribute = "employeeLeaveDateTime"
offsetInDays = 0 # fire exactly on the leave date
}
}
tasks = @(
@{ category = "leaver"; displayName = "Disable user account"; isEnabled = $true
taskDefinitionId = "1dfdfcc7-52fa-4c2e-bf3a-e3919cc12950"; arguments = @() }
@{ category = "leaver"; displayName = "Remove user from all groups"; isEnabled = $true
taskDefinitionId = "b3a31406-2a15-4c9a-b25b-a658fa5f07fc"; arguments = @() }
@{ category = "leaver"; displayName = "Remove all licenses for user"; isEnabled = $true
taskDefinitionId = "8fa97d28-3e52-4985-b3a9-a1126f9b8b4e"; arguments = @() }
@{ category = "leaver"; displayName = "Revoke all refresh tokens for user"; isEnabled = $true
taskDefinitionId = "509589a4-0466-4471-829e-49c5e502bdee"; arguments = @() }
)
}
Order matters: revoke tokens after disabling and stripping access, so an in-flight session cannot re-acquire anything before the tokens are killed. The built-in task catalogue you will actually use:
| Task | taskDefinitionId |
Category | Notes |
|---|---|---|---|
| Send welcome email to new hire | 70b29d51-b59a-4773-9280-8841dfd3f2ea |
joiner | Customizable HTML; fires pre-hire |
| Add user to groups | 22085229-5809-45e8-97fd-270d28d66910 |
joiner, mover | Argument = target group IDs |
| Add user to teams | e440ffb9-... (per docs) |
joiner, mover | Teams membership |
| Enable user account | 6fc52c9d-398b-4305-9763-15f42c1676fc |
joiner, leaver | Reverse of disable |
| Run a custom task extension | 4262b724-8dba-4fad-afc3-43fcbb497a0e / d79d1fcc-16be-490c-a865-f4533b1639ee |
all | Calls an Azure Logic App (verify the current GUID in docs) |
| Disable user account | 1dfdfcc7-52fa-4c2e-bf3a-e3919cc12950 |
leaver, mover | Blocks sign-in |
| Remove user from selected groups | 1953a66c-751c-45e5-8bfe-01462c70da3c |
mover, leaver | Argument = specific groups |
| Remove user from all groups | b3a31406-2a15-4c9a-b25b-a658fa5f07fc |
leaver | Wholesale group strip |
| Remove all licenses for user | 8fa97d28-3e52-4985-b3a9-a1126f9b8b4e |
leaver | Frees license, may cascade to mailbox |
| Remove user from all teams | 2891e2b0-... (per docs) |
leaver | Teams cleanup |
| Revoke all refresh tokens | 509589a4-0466-4471-829e-49c5e502bdee |
leaver | Kills live sessions |
| Delete user account | 8d18588d-9ad3-4c0d-8e5f-... |
leaver | Terminal; usually gated far after departure |
Treat the GUIDs above as the reference set, but confirm the exact
taskDefinitionIdin the current Microsoft docs before pasting into production — a handful are versioned and a wrong GUID makes the task silently invalid. The three leaver GUIDs and the two joiner GUIDs shown in the code blocks are the stable, commonly used ones.
The joiner workflow — ready before day one
The joiner side triggers ahead of the hire date so accounts are ready on day one, not provisioned at 9 a.m. while the new hire waits. Set offsetInDays = -7 against employeeHireDate.
executionConditions = @{
"@odata.type" = "#microsoft.graph.identityGovernance.triggerAndScopeBasedConditions"
scope = @{
"@odata.type" = "#microsoft.graph.identityGovernance.ruleBasedSubjectSet"
rule = "(employeeType -eq 'Employee')"
}
trigger = @{
"@odata.type" = "#microsoft.graph.identityGovernance.timeBasedAttributeTrigger"
timeBasedAttribute = "employeeHireDate"
offsetInDays = -7 # provision a week before start
}
}
tasks = @(
@{ category = "joiner"; displayName = "Add to default groups"; isEnabled = $true
taskDefinitionId = "22085229-5809-45e8-97fd-270d28d66910"
arguments = @( @{ name = "groupID"; value = "aaaa-...all-staff" } ) }
@{ category = "joiner"; displayName = "Send welcome email"; isEnabled = $true
taskDefinitionId = "70b29d51-b59a-4773-9280-8841dfd3f2ea"; arguments = @() }
)
The mover workflow — the subtle one
The mover is the workflow people forget, and the one reviews were invented to backstop. A mover changed department, title, or manager; some access should follow them (new team’s groups), some should be stripped (old team’s groups), and much is invisible to any rule. Mover workflows trigger on a change to an attribute rather than a fixed date, so the trigger model differs — you scope by the attribute change and run on-demand or scheduled reconciliation, then let an access review catch the residue the workflow’s known-group list cannot.
Triggers and their attributes, side by side:
| Trigger type | @odata.type suffix |
Attribute / basis | Offset | JML fit |
|---|---|---|---|---|
| Time-based attribute | timeBasedAttributeTrigger |
employeeHireDate, employeeLeaveDateTime |
offsetInDays (±) |
Joiner (−7), Leaver (0) |
| Attribute-change | attributeChangeTrigger |
e.g. department, jobTitle |
n/a (on change) | Mover |
| On-demand | run action, no trigger | manual selection of users | n/a | Ad-hoc reprocessing/testing |
Execution-scope rule attributes (a subset — the rule uses the same filter syntax as dynamic groups):
| Attribute | Example rule fragment | Use |
|---|---|---|
department |
(department -eq 'Finance') |
Scope to a business unit |
employeeType |
(employeeType -eq 'Contractor') |
Different flows for staff vs contractors |
jobTitle |
(jobTitle -startsWith 'Senior') |
Seniority-based provisioning |
accountEnabled |
(accountEnabled -eq true) |
Exclude already-disabled accounts |
companyName |
(companyName -eq 'Acme EU') |
Multi-entity tenants |
Custom task extensions for systems Graph cannot reach
Built-in tasks cover Entra-native objects. For anything external — de-provisioning a Salesforce seat, releasing a phone number, archiving a mailbox via a third party, notifying a ticketing system — use the Run a Custom Task Extension task, which calls an Azure Logic App. Two execution modes matter:
- Launch and continue — fire the Logic App, do not wait. For fire-and-forget side effects where downstream success does not gate later tasks.
- Launch and wait — block the workflow on the Logic App’s callback (up to the configured timeout). Use when downstream success must gate a later task, e.g. confirm the external system disabled the account before you delete the Entra object.
| Aspect | Launch and continue | Launch and wait |
|---|---|---|
| Workflow behaviour | Proceeds immediately | Pauses until callback or timeout |
| Downstream gating | No | Yes — later tasks depend on success |
| Failure handling | Fire-and-forget | Callback reports success/failure |
| Use for | Notifications, best-effort side effects | De-provision that must complete first |
| Limit | Up to 100 custom task extensions per tenant | Same; treat the Logic App as the integration boundary |
Keep the workflow declarative and push all integration logic into the Logic App — that keeps the JML model readable and the vendor-specific glue in one place.
Recurring access reviews for groups, apps, roles, and packages
Lifecycle workflows catch the clean leaver. They do not catch the mover who changed teams but kept their old group, or access granted manually outside any package. That residue is what access reviews re-attest on a schedule.
A review is an accessReviewScheduleDefinition: a scope (what is reviewed), reviewers (who decides), and settings (recurrence, duration, decision helpers, and what happens to non-responses). The two settings that make a review a control rather than a survey are autoApplyDecisionsEnabled and defaultDecision — without auto-apply, “remove” decisions sit in a report nobody actions.
Review of a group’s membership
$params = @{
displayName = "Quarterly - Data Platform Admins group"
descriptionForAdmins = "Re-attest membership of the data platform admin group"
descriptionForReviewers = "Confirm each member still needs admin on the data platform."
scope = @{
"@odata.type" = "#microsoft.graph.accessReviewQueryScope"
query = "/groups/02f3bafb-448c-487c-88c2-5fd65ce49a41/transitiveMembers"
queryType = "MicrosoftGraph"
}
reviewers = @(
@{ query = "/groups/02f3bafb-448c-487c-88c2-5fd65ce49a41/owners"; queryType = "MicrosoftGraph" }
)
settings = @{
mailNotificationsEnabled = $true
reminderNotificationsEnabled = $true
justificationRequiredOnApproval = $true
recommendationsEnabled = $true # sign-in-based approve/deny hints (decision helper)
instanceDurationInDays = 14
autoApplyDecisionsEnabled = $true # ENFORCE the outcome
defaultDecisionEnabled = $true
defaultDecision = "Deny" # no response = revoke (deny-by-default)
recurrence = @{
pattern = @{ type = "absoluteMonthly"; dayOfMonth = 1; interval = 3 } # quarterly
range = @{ type = "noEnd"; startDate = "2026-07-01" }
}
}
}
New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $params
defaultDecision = "Deny" is the load-bearing choice. A reviewer who ignores the review removes the access by inaction. That is the only model that scales: it makes keeping access the deliberate act, not losing it. For lower-stakes reviews you may prefer "None" (no change on silence), but for privileged groups, deny-by-default is correct.
What you can review, and the scope for each
Each object type has a distinct scope query. Getting the query right is most of the work:
| Review target | Scope query pattern | @odata.type |
Reviewer choices |
|---|---|---|---|
| Group members | /groups/{id}/transitiveMembers |
accessReviewQueryScope |
Group owners, managers, self, named |
| Application (assigned users) | /servicePrincipals/{id}/appRoleAssignedTo |
accessReviewQueryScope |
App owners, managers, named |
| Directory role members (active) | PIM role scope query | principalResourceMembershipsScope |
Privileged-role admins, named |
| PIM-eligible role assignments | role eligibility scope | principalResourceMembershipsScope |
Same; reviews eligibility, not active |
| PIM group (eligible/active) | group + member/owner accessId |
group scope | Group owners |
| Access package assignments | accessPackageAssignments filter |
accessReviewQueryScope |
Package’s policy reviewers, managers, self |
| Inactive users (guests) | members filtered by inactivity | accessReviewInactiveUsersQueryScope |
Owners |
| All Microsoft 365 groups (bulk) | groups filter + per-group instances | scope + instanceEnumerationScope |
Owners per group |
Reviewer, recurrence, and decision-helper reference
Who reviews:
| Reviewer type | Query / config | Best for | Risk |
|---|---|---|---|
| Resource/group owners | /groups/{id}/owners or ./owners |
Owner knows who should have access | Owner may rubber-stamp; pair with helpers |
| Managers | managerLevel / manager query |
Membership tied to reporting line | Manager may not know app specifics |
| Self-review | self scope | Low-stakes attestation, “do you still need this?” | Users almost never remove their own access |
| Named reviewers | singleUser list |
Security team owns a sensitive review | Bottleneck if the list is short |
| Fallback reviewers | fallbackReviewers |
When primary reviewer is absent/unset | Configure so a review never stalls |
Recurrence patterns:
| Recurrence | pattern.type |
Key fields | Typical cadence |
|---|---|---|---|
| One-time | (no recurrence block) | — | Ad-hoc cleanup, existing-holder resolution |
| Weekly | weekly |
interval, daysOfWeek |
High-churn sensitive access |
| Monthly | absoluteMonthly |
dayOfMonth, interval |
Guests, standard privileged groups |
| Quarterly | absoluteMonthly + interval: 3 |
as above | SOX-style re-attestation |
| Semi-annual / annual | absoluteMonthly + interval: 6/12 |
as above | Low-risk, broad populations |
| Range end | range.type = noEnd / endDate / numbered |
startDate, endDate, numberOfOccurrences |
noEnd for standing programs |
Decision helpers and non-response behaviour — the settings that make a review scale:
| Setting | Values | Effect | Recommendation |
|---|---|---|---|
recommendationsEnabled |
true/false |
Show sign-in-based approve/deny hints | true — surfaces stale access to reviewers |
recommendationLookBackDuration |
ISO duration (e.g. P30D) |
Window used to judge “inactive” | Match your inactivity policy |
autoApplyDecisionsEnabled |
true/false |
Apply decisions automatically at end | true — the difference between control and report |
defaultDecisionEnabled + defaultDecision |
Deny/Approve/None |
Outcome for non-responders | Deny for privilege; None for low-stakes |
justificationRequiredOnApproval |
true/false |
Approver must justify to keep access | true — makes “approve” a deliberate act |
instanceDurationInDays |
integer | How long each cycle stays open | 7–14 days typically |
mailNotificationsEnabled / reminderNotificationsEnabled |
true/false |
Notify/remind reviewers | Both true |
For application access reviews, scope to the app’s assigned users; for privileged role reviews, prefer reviewing the PIM-eligible assignments (see the PIM section) over active role members — otherwise you review only who is currently active, missing everyone who is merely eligible.
Govern guests and external access
Guests are the highest-decay population in any tenant — project-based, rarely offboarded by the inviting employee, and invisible to HR-driven lifecycle workflows because they have no employeeLeaveDateTime. Three controls contain them: connected organizations for governed onboarding, inactive-guest reviews for decay, and externally visible catalogs for automatic lifecycle boundaries.
Connected organizations — governed B2B onboarding
A connected organization registers a partner tenant (or domain) so its users can request packages from your externally visible catalogs, with the partner’s sponsors as approvers. This is what lets you offer self-service to known partners without opening the door to any guest from any tenant.
# Register a partner tenant as a connected organization
New-MgEntitlementManagementConnectedOrganization -BodyParameter @{
displayName = "Contoso Partners"
description = "External auditors and co-dev partner"
state = "configured"
identitySources = @(
@{
"@odata.type" = "#microsoft.graph.azureActiveDirectoryTenant"
tenantId = "contoso.onmicrosoft.com-tenant-guid"
displayName = "Contoso"
}
)
}
You then scope an access package’s requestors to that connected organization, and set its sponsors as the approvers, so external requests are governed exactly like internal ones.
Inactive-guest reviews
Scope a review to guests who have not signed in for a defined window using the inactive-users scope, and let the team owners decide. Auto-apply with deny-by-default so silence removes the stale guest.
$guestReview = @{
displayName = "Inactive guests on M365 groups (90d)"
descriptionForAdmins = "Remove guest access to groups with no recent sign-in."
instanceEnumerationScope = @{
"@odata.type" = "#microsoft.graph.accessReviewQueryScope"
query = "/groups?`$filter=(groupTypes/any(c:c+eq+'Unified'))"
queryType = "MicrosoftGraph"
}
scope = @{
"@odata.type" = "#microsoft.graph.accessReviewInactiveUsersQueryScope"
query = "./members/microsoft.graph.user/?`$filter=(userType eq 'Guest')"
queryType = "MicrosoftGraph"
inactiveDuration = "P90D"
}
reviewers = @( @{ query = "./owners"; queryType = "MicrosoftGraph" } )
settings = @{
mailNotificationsEnabled = $true
instanceDurationInDays = 7
autoApplyDecisionsEnabled = $true
defaultDecisionEnabled = $true
defaultDecision = "Deny"
recurrence = @{ pattern = @{ type = "absoluteMonthly"; dayOfMonth = 1; interval = 1 }
range = @{ type = "noEnd"; startDate = "2026-07-01" } }
}
}
New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $guestReview
Catalog as the lifecycle boundary
Combine an externally visible catalog with entitlement management’s external-user settings so guests provisioned through the catalog are auto-removed when their last access package assignment expires — closing the orphaned-guest gap by construction. The catalog, not a manual cleanup, becomes the lifecycle boundary for external identities.
| Guest lifecycle stage | Control | Effect |
|---|---|---|
| Onboard | Connected org + externally visible catalog | Governed request, sponsor approval |
| Steady state | Package expiration |
Access self-expires unless re-requested |
| Decay | Inactive-guest review | Stale guests removed on non-response |
| Offboard | Catalog external-user setting: remove on last assignment expiry | Guest object cleaned up automatically |
| Boundary | Cross-tenant access settings | Which partner tenants may be invited in/out |
The external-user removal behaviours a catalog offers:
| Behaviour | What it does | When to use |
|---|---|---|
| Block external user from signing in | Disables sign-in when last assignment ends | Retain object for audit, deny access |
| Remove external user | Deletes the guest object when last assignment ends | Full cleanup; the strong default for project guests |
| Days before removal | Grace window after last assignment expires | Buffer against premature deletion |
For the full cross-tenant access model — inbound/outbound trust, MFA and compliant-device trust, and B2B direct connect — see Entra External ID B2B and Cross-Tenant Access: Guest Collaboration, Direct Connect, and Trust Settings Done Right.
Integrate with PIM for time-bound privilege
Reviews and packages govern which roles a person can hold. PIM governs when they hold them. For any high-value role, the assignment delivered by governance should be eligible, not active — the person activates just-in-time, with justification and a time box.
The clean pattern: an access package or lifecycle task makes someone a member of a PIM-enabled group, and that group is itself eligible (not active) for the privileged role. Governance controls group membership; PIM controls activation. Create the eligible group-to-member relationship via the privileged access group eligibility schedule request:
New-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleRequest -BodyParameter @{
accessId = "member" # eligible as a member of the group
principalId = "aaaaaaaa-0000-0000-0000-000000000001" # the user
groupId = "bbbbbbbb-0000-0000-0000-000000000002" # PIM-enabled privileged group
action = "adminAssign"
scheduleInfo = @{
startDateTime = (Get-Date).ToString("o")
expiration = @{ type = "afterDuration"; duration = "P90D" } # eligibility itself expires
}
justification = "Quarterly eligible assignment via governance"
}
Two expirations now stack: eligibility expires in 90 days (governed, re-attested by a review), and each activation expires in hours (PIM policy). A compromised account that has not activated holds no standing privilege, and even an active session is time-boxed. Then run an access review over the eligible assignments themselves so eligibility does not become the new standing privilege — PIM exposes reviews for eligible role and group assignments precisely for this.
How governance and PIM divide responsibility:
| Question | Answered by | Mechanism |
|---|---|---|
| Which role can this person hold? | Governance | Access package / lifecycle task → group membership |
| For how long may they be eligible? | Governance | Eligibility expiration (e.g. P90D), reviewed |
| When do they actually hold it? | PIM | Activation with justification/MFA |
| For how long per activation? | PIM | Activation max duration (hours) |
| Is the eligibility still justified? | Governance | Access review over eligible assignments |
| Was activation appropriate? | PIM + SecOps | PIM audit / activation alerts |
The two-expiration stack, concretely:
| Layer | Expires after | Governed by | If account is compromised |
|---|---|---|---|
| Group membership (via package) | Package expiry (e.g. 180d) | Entitlement management + review | Loses eligibility at expiry |
| Eligibility (group → role) | Eligibility expiry (e.g. 90d) | PIM assignment + review | Holds nothing until activation |
| Activation (role active) | Hours (PIM policy) | PIM activation | Even active session is time-boxed |
For the full PIM design — role settings, activation policies, approval on activation, and PAM patterns — see Privileged Identity Management and PAM Architecture: Just-in-Time Access at Scale.
Licensing: what you must own before you design
Governance features are licensed per governed principal, and the license gates the feature, not just the admin. Design to what you can license.
| Feature | Base P1 | Base P2 | Entra ID Governance / Suite | Note |
|---|---|---|---|---|
| Basic access reviews of groups/apps | — | Included | Included | P2 covers foundational reviews |
| PIM (roles) | — | Included | Included | P2 core capability |
| Entitlement management (access packages) | — | — | Required | Not in P1/P2 alone |
| Lifecycle workflows (JML) | — | — | Required | Governance/Suite feature |
| Access reviews of access packages / inactive users / PIM eligibility | — | Partial | Required for full scope | Advanced review scopes need Governance |
| Auto-assignment policies | — | — | Required | Attribute-driven assignment |
| Connected organizations / governed B2B | — | — | Required | External governance |
| Custom task extensions (Logic Apps) | — | — | Required | Plus Logic App consumption cost |
The licensing rules that catch teams out:
| Rule | Consequence |
|---|---|
| License every principal governed, not just admins | A leaver workflow over 9,000 staff needs 9,000 Governance licenses |
| Guests may consume governance too | External identities in governed catalogs count toward external licensing (MAU-based for CIAM-style, per Microsoft’s model) |
| Entra Suite bundles Governance + more | If you also want Internet Access / Private Access / Verified ID, the Suite may be cheaper than parts |
| Downgrading strands config | Removing Governance licenses disables the features but may leave orphaned definitions — plan exits |
Confirm before you build:
# What Governance-related service plans are enabled on the tenant's SKUs?
Get-MgSubscribedSku | Select-Object SkuPartNumber, `
@{n='Enabled';e={$_.PrepaidUnits.Enabled}}, @{n='Consumed';e={$_.ConsumedUnits}}
Architecture at a glance
Picture the governance machine as three intake lanes feeding one directory, with PIM as a gate on the privileged lane and audit as a tap on the output. There is no single diagram because the value is in how the lanes interlock, not in a box chart — so walk it as a flow.
Lane one is event-driven (lifecycle workflows). The HR/HCM system is the source of truth for employment: it sets employeeHireDate, employeeLeaveDateTime, department, and manager via inbound provisioning. Those attributes are the triggers. Seven days before a hire date, the joiner workflow adds default groups and sends a welcome email; on the leave date, the leaver workflow disables the account, strips groups and licenses, and revokes tokens; on an attribute change, the mover workflow reconciles known groups. Anything external (SaaS seat, phone number) is a custom task extension calling a Logic App. This lane needs no human in the loop — the employment event is the authorization.
Lane two is request-driven (entitlement management). A user opens the My Access portal, requests an access package from a delegated catalog, and one or more approval stages decide. Separation-of-duties gates block toxic combinations at this exact point — the request for the incompatible package never completes. The grant is time-bound; it self-destructs on expiration, forcing a re-request. Partners flow through the same lane via connected organizations and externally visible catalogs, with sponsors as approvers.
Lane three is attribute-driven (auto-assignment). Birthright access — the Finance apps for Finance staff — is granted by a membership rule with no request, and removed when the rule stops matching. This lane overlaps the mover workflow deliberately: when someone leaves Finance, both the auto-assignment removal and the mover workflow act, and the belt-and-suspenders is intentional.
The privileged gate (PIM). Where a lane would grant a privileged role, it instead grants membership of a PIM-enabled group that is merely eligible for the role. The user must activate through PIM — justification, MFA, a time box — so no standing privilege exists. Eligibility itself expires and is re-attested.
The re-attestation loop and the audit tap (access reviews + AuditLogs). Over all three lanes, recurring access reviews re-attest what the lanes left behind — the mover’s residual groups, the manual grant outside any package, the eligible assignment that outlived its need — with deny-by-default and auto-apply so silence removes access. Every action any lane takes lands in AuditLogs (workflow runs, package grants, review decisions), which is the evidence tap: you prove the machine ran from the log, never from the config. Read left to right — HR event / user request / attribute match on the left, a governed and time-boxed directory in the middle, PIM gating privilege, and an audit trail on the right — and the whole system is one picture.
Real-world scenario
A 9,000-employee insurer ran SOX and faced a finding that hits every regulated firm eventually. Claims adjusters could request the “Claims Write” access package and the “Payment Release” package, and 14 people held both. That combination let one person file and pay a fraudulent claim end to end. The external auditor wanted not just the toxic pairs removed, but a preventive control proving the combination could never recur — a quarterly cleanup spreadsheet was explicitly rejected as a detective control.
The platform team’s first instinct was a Sentinel rule to alert when both assignments coexisted. That failed the auditor for the right reason: it detects the violation after the access exists, leaving an exploitable window. The fix moved the control to request time. They marked the two packages mutually incompatible in entitlement management, so the second request is blocked — no toxic state is reachable. They then layered a quarterly review (deny-by-default) over the Payment Release package as recurring re-attestation on top of the preventive gate.
# Preventive SoD: Payment Release becomes unrequestable for anyone holding Claims Write
New-MgEntitlementManagementAccessPackageIncompatibleAccessPackageByRef `
-AccessPackageId $paymentReleaseId `
-BodyParameter @{
"@odata.id" = "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/accessPackages/$claimsWriteId"
}
The reciprocal reference was added on the Claims Write package so the block holds in both directions. The 14 existing dual-holders were resolved through a one-time targeted review rather than bulk removal, preserving the access each legitimately needed. But the finding also exposed two adjacent gaps the auditor then probed. First, offboarding: three of the 14 had left the company months earlier and their accounts were still enabled — manual offboarding via a ticket queue had missed them. The team stood up a leaver workflow on employeeLeaveDateTime (disable, strip groups/licenses, revoke tokens) so departure de-provisioning stopped depending on a human remembering. Second, the “Payment Release” package granted an app role that was effectively privileged; the team re-plumbed it so the grant delivered membership of a PIM-enabled group that was eligible for the role, with activation requiring approval and MFA — turning standing capability into just-in-time.
The numbers told the story at the next audit. Standing dual-holders: zero, structurally impossible. Mean time to de-provision a leaver: from “up to weeks, ticket-dependent” to under an hour, on the leave date, with a run-history record per user. Reviewer effort on the Payment Release review dropped because the decision helper flagged the two members who had not signed in in 60 days, and deny-by-default removed a third who ignored the review entirely. The auditor signed off on the incompatibility configuration, the leaver workflow run history, and the review schedule as a combined preventive-and-detective control. The lesson the team carried forward: SoD belongs at the request gate, not in the SIEM, and offboarding belongs on the HR trigger, not in a ticket queue. A guardrail that prevents the state beats an alarm that reports it.
Advantages and disadvantages
The honest trade-off before you commit a program:
| Advantages | Disadvantages |
|---|---|
| Revocation keeps pace with granting (expiry, JML, reviews) | Requires Entra ID Governance/Suite licensing per governed principal |
| Preventive SoD makes toxic combinations structurally impossible | Attribute-driven pieces are only as good as the HR data feeding them |
| Delegated catalogs kill the central-IT ticket bottleneck | Poor catalog design (one giant catalog) recreates the bottleneck |
| Deny-by-default reviews scale reviewer effort down | Rubber-stamping reviewers can undermine the control if helpers unused |
| Real-time leaver de-provisioning with audit-grade run history | Workflow depends on populated employeeLeaveDateTime |
| PIM interplay removes standing privilege entirely | Added activation friction for admins; more moving parts |
| Governed B2B via connected orgs closes the guest-decay gap | External identity licensing/consumption adds cost |
| Everything is Graph-configurable and repeatable as code | Steep initial design; not a checkbox you flip on |
Where each advantage matters most: preventive SoD is decisive in regulated finance/healthcare where auditors demand it; real-time leaver workflows matter most where high staff churn or contractor populations make manual offboarding untenable; PIM interplay is essential wherever a compromised admin account is the top risk; delegated catalogs pay off hardest in large tenants where central IT cannot know every access need. Conversely, a 200-person startup with clean HR data and few privileged roles may get 80% of the value from access packages with expiry plus a leaver workflow, and defer the rest.
Hands-on lab
A safe, end-to-end walk-through in a non-production tenant: a delegated catalog, an expiring access package with manager approval, a leaver lifecycle workflow (created disabled), an enforcing access review, then verification and teardown. Requires Entra ID Governance licensing on the tenant and the scopes from Prerequisites. Use throwaway test users.
Step 0 — Connect and create test objects.
Connect-MgGraph -Scopes @(
"EntitlementManagement.ReadWrite.All","AccessReview.ReadWrite.All",
"LifecycleWorkflows.ReadWrite.All","Group.ReadWrite.All","User.ReadWrite.All")
# A test group to govern, and a test user
$grp = New-MgGroup -DisplayName "LAB-DataPlatform-Analysts" -MailEnabled:$false `
-MailNickname "lab-dp-analysts" -SecurityEnabled:$true
"Group: $($grp.Id)"
Expected: a group object id prints. Note it as $grp.Id.
Step 1 — Create a delegated catalog and add the group as a resource.
$cat = New-MgEntitlementManagementCatalog -BodyParameter @{
displayName = "LAB - Data Platform"; description = "Lab catalog"; isExternallyVisible = $false }
New-MgEntitlementManagementResourceRequest -BodyParameter @{
requestType = "AdminAdd"; catalog = @{ id = $cat.Id }
resource = @{ originId = $grp.Id; originSystem = "AadGroup" } }
"Catalog: $($cat.Id)"
Expected: catalog id prints; the resource-add request completes. Validate: Get-MgEntitlementManagementCatalogResource -AccessPackageCatalogId $cat.Id lists the group.
Step 2 — Create an access package with a 30-day, manager-approval policy.
$pkg = New-MgEntitlementManagementAccessPackage -BodyParameter @{
displayName = "LAB - Analyst read"; description = "Lab package"; catalog = @{ id = $cat.Id } }
New-MgEntitlementManagementAssignmentPolicy -BodyParameter @{
displayName = "LAB - 30 day manager approval"; accessPackage = @{ id = $pkg.Id }
requestorSettings = @{ enableTargetsToSelfAddAccess = $true; onBehalfRequestorsAllowed = $false }
requestApprovalSettings = @{ isApprovalRequiredForAdd = $true
stages = @( @{ durationBeforeAutomaticDenial = "P7D"; isApproverJustificationRequired = $true
primaryApprovers = @( @{ "@odata.type"="#microsoft.graph.requestorManager"; managerLevel=1 } ) } ) }
expiration = @{ type = "afterDuration"; duration = "P30D" } }
"Package: $($pkg.Id)"
Expected: package id prints. Validate: the package appears in the My Access portal for an in-scope user, showing “requires approval” and a 30-day duration.
Step 3 — Create a leaver workflow, disabled, for safe inspection.
New-MgIdentityGovernanceLifecycleWorkflow -BodyParameter @{
displayName = "LAB - Leaver (disabled)"; description = "Lab only"; category = "leaver"
isEnabled = $false # created OFF so it cannot fire
executionConditions = @{
"@odata.type" = "#microsoft.graph.identityGovernance.triggerAndScopeBasedConditions"
scope = @{ "@odata.type" = "#microsoft.graph.identityGovernance.ruleBasedSubjectSet"
rule = "(department -eq 'LAB-NONEXISTENT')" } # matches nobody
trigger = @{ "@odata.type" = "#microsoft.graph.identityGovernance.timeBasedAttributeTrigger"
timeBasedAttribute = "employeeLeaveDateTime"; offsetInDays = 0 } }
tasks = @(
@{ category="leaver"; displayName="Disable user account"; isEnabled=$true
taskDefinitionId="1dfdfcc7-52fa-4c2e-bf3a-e3919cc12950"; arguments=@() }
@{ category="leaver"; displayName="Revoke all refresh tokens"; isEnabled=$true
taskDefinitionId="509589a4-0466-4471-829e-49c5e502bdee"; arguments=@() } ) }
Expected: the workflow is created with isEnabled=$false and a scope matching nobody — doubly safe. Validate: Get-MgIdentityGovernanceLifecycleWorkflow -Filter "displayName eq 'LAB - Leaver (disabled)'" shows it disabled.
Step 4 — Create an enforcing access review of the group.
New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter @{
displayName = "LAB - Analyst group review"
descriptionForAdmins = "Lab"; descriptionForReviewers = "Confirm membership"
scope = @{ "@odata.type"="#microsoft.graph.accessReviewQueryScope"
query = "/groups/$($grp.Id)/transitiveMembers"; queryType = "MicrosoftGraph" }
reviewers = @( @{ query = "/groups/$($grp.Id)/owners"; queryType = "MicrosoftGraph" } )
settings = @{ mailNotificationsEnabled=$true; recommendationsEnabled=$true
instanceDurationInDays=3; autoApplyDecisionsEnabled=$true
defaultDecisionEnabled=$true; defaultDecision="Deny"
recurrence = @{ pattern=@{ type="absoluteMonthly"; dayOfMonth=1; interval=3 }
range=@{ type="numbered"; startDate="2026-07-01"; numberOfOccurrences=1 } } } }
Expected: a review definition is created (one occurrence, so it self-terminates). Validate: Get-MgIdentityGovernanceAccessReviewDefinition -Filter "displayName eq 'LAB - Analyst group review'" returns it with autoApplyDecisionsEnabled = true and defaultDecision = Deny.
Step 5 — Verify the machinery (read-only).
# Packages and policies exist and are time-bound
Get-MgEntitlementManagementAccessPackage -Filter "displayName eq 'LAB - Analyst read'" |
Select-Object DisplayName, Id
# Workflow is present and safely disabled
(Get-MgIdentityGovernanceLifecycleWorkflow -Filter "displayName eq 'LAB - Leaver (disabled)'").IsEnabled
# Review enforces (auto-apply + deny default)
$def = Get-MgIdentityGovernanceAccessReviewDefinition -Filter "displayName eq 'LAB - Analyst group review'"
$def.Settings | Select-Object AutoApplyDecisionsEnabled, DefaultDecision
Expected: package prints; workflow IsEnabled is False; review shows True / Deny.
Step 6 — Teardown. Remove everything you created, in reverse order.
# Delete the review, workflow, package, catalog, and test group
Remove-MgIdentityGovernanceAccessReviewDefinition -AccessReviewScheduleDefinitionId $def.Id
$wf = Get-MgIdentityGovernanceLifecycleWorkflow -Filter "displayName eq 'LAB - Leaver (disabled)'"
Remove-MgIdentityGovernanceLifecycleWorkflow -LifecycleWorkflowId $wf.Id
Remove-MgEntitlementManagementAccessPackage -AccessPackageId $pkg.Id
Remove-MgEntitlementManagementCatalog -AccessPackageCatalogId $cat.Id
Remove-MgGroup -GroupId $grp.Id
"Torn down."
Expected: each removal returns without error and “Torn down.” prints. Validate: re-running any Get-...-Filter returns nothing.
Common mistakes & troubleshooting
The failure modes that actually bite in production — symptom, root cause, how to confirm, and the fix.
| # | Symptom | Root cause | Confirm (exact command / portal path) | Fix |
|---|---|---|---|---|
| 1 | Feature simply not available / “requires a license” | Entra ID Governance/Suite not assigned to the governed principals | Get-MgSubscribedSku shows no Governance plan; portal banner |
Assign Governance/Suite to every governed user (incl. guests) |
| 2 | Leaver workflow never fires | employeeLeaveDateTime not populated by HR provisioning |
Get-MgUser -UserId <id> -Property employeeLeaveDateTime is null |
Fix inbound provisioning mapping; backfill the attribute |
| 3 | Workflow fires but task fails for some users | Task target missing (group deleted) or insufficient privilege | Get-MgIdentityGovernanceLifecycleWorkflowRunTaskProcessingResult: FailureReason |
Correct task arguments; re-run for failed users |
| 4 | SoD block only works one way | Incompatible reference set on one package, not the mirror | Portal → package → Separation of duties (each package) | Add the reciprocal IncompatibleAccessPackageByRef |
| 5 | Users still hold a toxic pair after SoD | SoD is preventive; existing holders not auto-removed | Query assignments for both packages | One-time targeted review to resolve existing holders |
| 6 | Review “completed” but nobody lost access | autoApplyDecisionsEnabled = false (report-only) |
$def.Settings.AutoApplyDecisionsEnabled = false |
Set it true; decisions then enforce automatically |
| 7 | Non-responders kept access | defaultDecision = None (no change on silence) |
$def.Settings.DefaultDecision = None |
Set Deny for privileged scopes |
| 8 | Whole app/group emptied by a review | Deny-by-default + reviewers ignored the review | Decision export shows mass Deny by system |
Re-grant needed access; add reminders/escalation; reconsider default for low-stakes |
| 9 | Auto-assignment grants nobody | Membership rule syntax wrong or attribute unpopulated | Portal shows 0 in policy; test rule as a dynamic group | Fix membershipRule; ensure attributes are set |
| 10 | Auto-assignment never removes access | removeAccessWhenTargetLeavesAllowedTargets = false |
Inspect the auto-assignment policy | Enable removal (+ grace period) |
| 11 | Guest never offboarded despite expiry | Catalog external-user removal not configured | Catalog → Settings → external user lifecycle | Set “remove/block on last assignment expiry” |
| 12 | Approver never sees the request | Approver type resolves to nobody (no manager set) | Request stuck; manager unset on requestor |
Set manager, or use singleUser/groupMembers; enable escalation |
| 13 | Privileged access still “standing” after governance | Package grants active role, not eligibility | Role shows active assignment, not eligible | Deliver via PIM-enabled group + eligibility; activate via PIM |
| 14 | Review of a role misses most holders | Reviewed active members only, not PIM-eligible | Review scope = active membership | Scope the review to eligible assignments |
| 15 | Workflow ran but external system unchanged | Built-in tasks don’t touch external systems | Salesforce/HR seat still active | Add a custom task extension (Logic App) for the external system |
| 16 | Delegated owner can’t add a resource | Owner lacks the directory privilege the resource needs | Add fails for a directory-role resource | Grant matching privilege, or have central IT add it |
Three expanded root-causes for the ones that waste the most time:
Leaver workflow never fires (#2). The most common governance disappointment. The workflow is enabled, scoped correctly, and still does nothing — because the trigger attribute is empty. Time-based triggers fire off employeeLeaveDateTime/employeeHireDate, and if HR provisioning never writes those attributes, there is no event. Confirm with Get-MgUser -UserId <id> -Property employeeLeaveDateTime,employeeHireDate; if null, the fix is in the provisioning mapping (Workday/SuccessFactors attribute flow) or Entra Connect, not in the workflow.
Review completed but nobody lost access (#6/#7). A review can finish and change nothing in two ways: auto-apply is off (decisions are recorded but never enforced — the classic “report we email to nobody”), or the default decision is None so silence keeps access. Both feel like the review “ran.” Confirm both settings on the definition; for any privileged scope, autoApplyDecisionsEnabled = true and defaultDecision = Deny together are what make a review a control.
Privileged access still standing after governance (#13). Teams often grant a privileged app role or directory role directly through a package, achieving time-bound-but-still-standing access. The intended pattern delivers membership of a PIM-enabled group that is merely eligible; the user then activates through PIM. Confirm by checking whether the role assignment is active or eligible; the fix re-plumbs the package/task to grant group membership and moves the role relationship to PIM eligibility.
Best practices
- License first, design second. Confirm Entra ID Governance/Suite covers every governed principal — including guests — before you scope a feature to your whole workforce. A workflow you cannot license is a design you cannot ship.
- Split catalogs by business domain and delegate ownership. One catalog per domain (Finance, Engineering, Data Platform), owned by that team. A single central catalog recreates the ticket bottleneck governance exists to remove.
- Every request policy sets
expiration. No perpetual grants.afterDuration(e.g.P180D) makes re-request the default and standing access the exception. - Model toxic combinations as incompatible packages, in both directions. Preventive SoD at the request gate beats any SIEM alert. Add the reciprocal reference or the block is one-way.
- Reviews must enforce:
autoApplyDecisionsEnabled = trueand deny-by-default for privilege. A review without auto-apply is a survey; a review that keeps access on silence is theatre. - Turn on decision helpers and reminders.
recommendationsEnabledsurfaces stale access; reminders and escalation stop a review stalling on one absent reviewer. - Leaver workflow on
employeeLeaveDateTime, in order: disable → strip → revoke. Revoke tokens last so an in-flight session cannot re-acquire access mid-teardown. Provision joiners ahead of the hire date. - Back mover workflows with a review. The mover workflow strips known groups; a periodic review catches everything the rule cannot see.
- Deliver privileged access as PIM-eligible, never active. Governance controls membership of a PIM-enabled group; PIM controls activation. Then review the eligibility.
- Govern guests through connected orgs and catalog auto-removal. Make the catalog the lifecycle boundary so guests self-clean when their last assignment expires.
- Configure fallback and escalation on every approval and review. No control should stall because one person is on holiday.
- Prove it ran from
AuditLogs, not config. An enabled workflow is not a workflow that ran; a scheduled review is not a review that applied. Verify from the log and export decisions per cycle.
Security notes
- Least-privilege Graph scopes and delegated roles. Grant
EntitlementManagement.ReadWrite.Alland friends only to the governance automation identity; delegate catalog ownership to domain teams so no one needs tenant-wide admin to run their own access. A delegated catalog owner cannot grant privilege they do not themselves hold — the boundary is real. - Separation of duties is a security control, not just compliance. Incompatible packages structurally prevent one person from holding a fraud-enabling combination. This closes the exposure window entirely, unlike detection.
- Deny-by-default reviews are a containment mechanism. For privileged groups and eligible PIM assignments, non-response removes access — the safe failure mode. Reserve
Nonefor genuinely low-stakes reviews. - Real-time leaver de-provisioning shrinks the breach window. Disabling and token-revocation on the leave date means a departed (or terminated-for-cause) employee loses access in minutes, not weeks — the difference between an orderly exit and an insider incident.
- PIM eligibility removes standing privilege. A compromised account that has not activated holds nothing worth stealing; even an activated session is time-boxed. Governance delivering eligibility (not active roles) is a core Zero Trust move — see Zero Trust on Microsoft Entra: Conditional Access + PIM, Step by Step.
- Protect the automation identity and the Logic Apps. The identity running governance Graph calls, and the Logic Apps behind custom task extensions, are high-value — secure them with managed identity, least-privilege, and monitoring, per Managed Identities Deep Dive: User-Assigned Identities, Federated Credentials, and RBAC Patterns for Azure Workloads.
- Break-glass accounts must be excluded and separately monitored. Governance workflows and reviews should never disable or strip your emergency-access accounts; exclude them explicitly and watch them, per Engineering Break-Glass Emergency Access Accounts in Entra ID: Exclusions, Hardening, and Tamper-Evident Monitoring.
- Audit evidence is a security asset. The per-decision review record (
reviewedBy,decision,justification, timestamp) and workflow run history are your defensible answer to “prove access was removed / reviewed.” Export and retain them.
Cost & sizing
- Licensing dominates. Entra ID Governance (or the Entra Suite) is licensed per governed principal. If you scope lifecycle workflows and advanced reviews to your entire 9,000-person workforce, you license 9,000 principals — this is the single largest cost lever, so scope deliberately. In INR terms, budget by (per-user Governance list price) × (governed headcount, including active guests), and compare the standalone Governance add-on against the Entra Suite bundle if you also want Internet/Private Access or Verified ID.
- Guests count. External identities consuming governed catalogs fall under Microsoft’s external-user licensing model (MAU-based for customer scenarios); a large partner population is a real line item, not free.
- Logic Apps consumption. Custom task extensions run Azure Logic Apps — a small per-execution + connector cost. At leaver scale (say 300 departures/month, each firing two external de-provision actions) this is modest (low thousands of INR/month) but non-zero; use Consumption-tier Logic Apps and keep the workflow declarative.
- Compute is otherwise negligible. Entitlement management, reviews, and workflows are platform features — no VM or per-transaction Graph cost beyond your normal tenant. The “cost” is design and operational effort, front-loaded.
- The savings are in avoided incidents and audit hours. Preventing one standing-privilege breach or one failed SOX audit dwarfs the licensing. Real-time leaver de-provisioning also reclaims IT hours previously spent on manual offboarding tickets.
A rough monthly picture for the 9,000-employee insurer: Governance/Suite licensing for the governed workforce is the headline figure (negotiated per-user, so tenant-specific), plus a few thousand INR for Logic Apps behind external de-provisioning, plus normal Log Analytics ingestion for the AuditLogs you export as evidence. The controls themselves add no compute bill — the discipline is the investment.
| Cost driver | What you pay for | Rough scale (INR/month) | What it buys | Watch-out |
|---|---|---|---|---|
| Governance/Suite licensing | Per governed principal | Headline; per-user × headcount | The features at all | License every governed user, incl. guests |
| External-user licensing | Guests in governed catalogs | MAU-based | Governed B2B | Large partner base scales cost |
| Logic Apps (custom tasks) | Per-execution + connectors | Low thousands | External-system de-provision | Use Consumption tier; keep logic external |
| Log Analytics ingestion | AuditLogs evidence retention |
~₹1,000–3,000 | Audit-grade proof | Set retention to your compliance window |
| Design / operations | Engineer time (front-loaded) | One-off + ongoing | A working program | Not a checkbox; budget the build |
Interview & exam questions
1. What is the difference between entitlement management, access reviews, and lifecycle workflows, and when do you use each? Entitlement management governs requested/attribute-driven access via access packages with policies (approval, expiry, SoD). Access reviews re-attest existing access on a schedule to remove creep and residue. Lifecycle workflows automate event-driven joiner-mover-leaver access on HR triggers. Use packages for discretionary and birthright access, workflows for employment-event provisioning/de-provisioning, and reviews for periodic re-certification of whatever the other two leave behind.
2. Walk the entitlement-management hierarchy. Catalog (container + delegation boundary) → resource (group/app/site added to the catalog) → resource role (membership, app role, permission level) → access package (a bundle of resource roles that is requestable) → assignment policy (who can request/get it, approval stages, and expiration). The policy is where governance lives; a package without a policy grants nothing.
3. How does separation of duties in entitlement management differ from a SIEM alert on toxic combinations? SoD is preventive and enforced at request time — a user holding an incompatible package cannot even request the conflicting one, so the toxic state is never reachable. A SIEM alert is detective — it fires after the access exists, leaving an exploitable window. SoD is a guardrail; the alert is an alarm. Auditors increasingly require the guardrail. Remember to configure both directions.
4. What two access-review settings turn a review from a report into a control? autoApplyDecisionsEnabled = true (decisions are enforced automatically at cycle end, not left in a report) and defaultDecision = Deny with defaultDecisionEnabled = true (non-response removes access). Together they make keeping access the deliberate act and losing it the default — the only model that scales for privileged scopes.
5. Design a leaver workflow. What triggers it, what tasks, in what order, and what’s the prerequisite? Trigger: time-based on employeeLeaveDateTime, offsetInDays = 0. Tasks in order: disable account → remove from all groups → remove all licenses → revoke all refresh tokens (revoke last so an in-flight session can’t re-acquire access). Prerequisite: employeeLeaveDateTime must be populated by HR-driven inbound provisioning, or nothing fires.
6. What is an auto-assignment policy and how does it differ from a request policy? An auto-assignment policy grants an access package’s roles to every user matching an attribute rule (e.g. department -eq 'Finance'), with no request and no approval — the rule is the gate — and removes access when the user stops matching (with an optional grace period). A request policy is self-service with approval stages and a timer-based expiry. Use auto-assignment for birthright access that tracks HR data; use request policies for discretionary access.
7. How do governance and PIM divide responsibility for a privileged role? Governance controls which role a person can hold and for how long they’re eligible — typically by making them a member of a PIM-enabled group that is eligible (not active) for the role, with the eligibility itself expiring and re-attested by a review. PIM controls when they actually hold it — activation with justification/MFA and an hours-long time box. Two expirations stack; no standing privilege exists.
8. A leaver workflow is enabled and correctly scoped but never runs. What’s the most likely cause and how do you confirm? The trigger attribute is empty. Time-based triggers fire off employeeLeaveDateTime; if HR provisioning never writes it, there is no event. Confirm with Get-MgUser -UserId <id> -Property employeeLeaveDateTime; if null, fix the provisioning attribute mapping or backfill.
9. How do you govern the lifecycle of B2B guests that have no employeeLeaveDateTime? Onboard them through connected organizations into an externally visible catalog (sponsor approval), give the package an expiration so access self-expires, run recurring inactive-guest reviews (deny-by-default) to catch decay, and configure the catalog’s external-user setting to remove/block the guest when their last assignment expires — making the catalog the lifecycle boundary. Constrain who can be invited with cross-tenant access settings.
10. Why review PIM-eligible assignments rather than only active role members? Because active membership shows only who is currently activated; the standing risk is everyone who is eligible and could activate. Reviewing only active members misses the population you most need to re-certify. PIM exposes reviews for eligible role and group assignments precisely so eligibility does not silently become the new standing privilege.
11. What is a custom task extension and when do you need one? A lifecycle-workflow task that calls an Azure Logic App, used to reach systems Graph cannot touch — de-provision a Salesforce seat, release a phone number, notify a ticketing system. It runs in “launch and continue” (fire-and-forget) or “launch and wait” (block on callback, up to timeout) mode; use “wait” when a later task depends on the external action completing. Up to 100 per tenant.
12. Which licensing is required for entitlement management and lifecycle workflows, and what’s the common trap? Microsoft Entra ID Governance (or the Entra Suite) — not base P1 or P2. The trap: the license is required per governed principal, not just the admin, so a workflow over 9,000 employees needs 9,000 Governance licenses, and guests in governed catalogs count too. Confirm licensing before designing scope.
These map to SC-300 (Identity and Access Administrator) — implement and manage identity governance (entitlement management, access reviews, PIM, entitlement lifecycle) — and touch SC-100 (Cybersecurity Architect) for the Zero Trust and least-privilege framing. Cert-mapping for revision:
| Question theme | Primary cert | Objective area |
|---|---|---|
| Entitlement management hierarchy, policies, SoD | SC-300 | Plan & implement entitlement management |
| Access reviews (scopes, auto-apply, defaults) | SC-300 | Plan & implement access reviews |
| Lifecycle workflows (JML, triggers, tasks) | SC-300 | Implement identity lifecycle |
| PIM interplay, eligibility, JIT | SC-300 | Implement PIM |
| Governed B2B, connected orgs | SC-300 | Manage external identities governance |
| Zero Trust / least-privilege framing | SC-100 | Design identity security strategy |
Quick check
- You need every Finance employee to automatically get the Finance apps, with no request form, and to lose them if they leave Finance. Which policy type, and which one flag makes the “lose them” part work?
- A quarterly access review of a privileged group “completed” but no one lost access, even people who never responded. Name the two settings you check and their correct values for a privileged scope.
- Two access packages, “Vendor Onboarding” and “Payment Approval”, must never be held together. What control do you use, and what is the one configuration step people forget?
- Your leaver workflow is enabled and scoped to Engineering, but nobody is ever de-provisioned. What is the single most likely cause?
- Governance made a user a member of a group that grants an app role, and the app role is privileged. What did you get wrong from a Zero-Trust standpoint, and what’s the fix?
Answers
- An auto-assignment policy (attribute rule
department -eq 'Finance', no request/approval). The flag that removes access when they no longer match isremoveAccessWhenTargetLeavesAllowedTargets = true(optionally with a grace period). autoApplyDecisionsEnabledmust betrue(otherwise decisions are recorded but never enforced — report-only) anddefaultDecisionmust beDenywithdefaultDecisionEnabled = true(otherwise non-response defaults toNoneand keeps access). For a privileged group, both together are required.- Separation of duties via incompatible access packages — mark each package incompatible with the other. The forgotten step is reciprocity: set the incompatible reference on both packages, or the block only works in one direction.
employeeLeaveDateTimeis not populated on those users (the time-based trigger has no event to fire on). Fix the HR-driven inbound provisioning mapping that should write it. Confirm withGet-MgUser -Property employeeLeaveDateTime.- You delivered a privileged role as standing (active) access rather than eligibility. The fix: make the group a PIM-enabled group that is eligible (not active) for the role, so the user must activate through PIM (justification, MFA, time box) — and review the eligibility.
Glossary
- Entitlement management — the Entra ID Governance feature for self-service, policy-governed access via access packages.
- Catalog — a container of governed resources plus a delegation boundary; resource owners manage their catalog without tenant-wide admin.
- Resource / resource role — a group/app/site added to a catalog, and a specific role on it (membership, app role, permission level) that a package can grant.
- Access package — a bundle of resource roles that users request or are auto-assigned as one unit.
- Assignment policy — the policy on an access package defining who may request/get it, approval stages, and expiration; where governance actually lives.
- Request policy vs auto-assignment policy — request = self-service with approval and timer expiry; auto-assignment = attribute-rule grant with no request, removed when the rule stops matching.
- Separation of duties (SoD) — a preventive request-time gate marking two packages (or a package and a group) incompatible, so a holder of one cannot request the other.
- Access review — a scheduled re-attestation (
accessReviewScheduleDefinition) of a scope by reviewers, with recurrence, decision helpers, and non-response handling. - Decision helper (recommendation) — a sign-in-based approve/deny hint shown to reviewers, driven by recent inactivity.
- Auto-apply / default decision —
autoApplyDecisionsEnabledenforces review outcomes automatically;defaultDecision(Deny/Approve/None) is applied to non-responders. - Lifecycle workflow — a trigger + execution scope + ordered tasks automating joiner/mover/leaver access.
- Task definition (
taskDefinitionId) — a stable GUID identifying a built-in workflow action (disable, remove from groups, revoke tokens, etc.). - Custom task extension — a workflow task that calls an Azure Logic App to reach systems Graph cannot; “launch and continue” or “launch and wait”.
- Trigger (time-based / attribute-change / on-demand) — what starts a workflow: an offset from
employeeHireDate/employeeLeaveDateTime, an attribute change, or a manual run. - Execution scope — a dynamic-group-style rule selecting which users a workflow applies to.
- Connected organization — a registered external tenant/domain whose users may request packages from externally visible catalogs, with sponsor approval.
- PIM-enabled group — a group whose membership can be made eligible (not active), bridging governance to just-in-time privilege.
- Eligibility vs activation — governance grants eligibility (months, re-attested); PIM grants activation (hours, with justification/MFA) on top of it.
- Entra ID Governance / Entra Suite — the licensing that unlocks entitlement management, lifecycle workflows, and advanced access reviews, per governed principal.
Next steps
You can now stand up the full governance machine and prove it runs. Build outward:
- Next: Privileged Identity Management and PAM Architecture: Just-in-Time Access at Scale — the JIT layer governance delivers eligibility into; get activation policies and PAM patterns right.
- Related: Zero Trust on Microsoft Entra: Conditional Access + PIM, Step by Step — governance decides what a principal may hold; Conditional Access decides under what conditions they use it.
- Related: Entra External ID B2B and Cross-Tenant Access: Guest Collaboration, Direct Connect, and Trust Settings Done Right — the full cross-tenant trust model behind governed guest onboarding.
- Related: Designing Conditional Access at Scale: A Persona-Based Policy Framework with Authentication Context and Filters — the persona model that pairs naturally with catalog and package design.
- Related: Engineering Break-Glass Emergency Access Accounts in Entra ID: Exclusions, Hardening, and Tamper-Evident Monitoring — the accounts your workflows and reviews must always exclude.
- Related: Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars — where identity governance sits in the broader Zero Trust picture.