In this module

IAM0.1 What Identity Governance Actually Is

6 hours · Module 0 · Free
What you already know

You've managed user accounts — creating users, assigning licenses, resetting passwords, maybe configuring MFA or Conditional Access. This section draws the line between administering identity and governing it, and makes that line concrete with Graph API output from your own tenant that shows exactly where governance exists, where it's absent, and what the gap looks like in real identity data.

What governance means — and why administration alone fails

Your tenant has user accounts, group memberships, app registrations, and maybe lifecycle workflows or access reviews. Someone creates accounts when people join. Someone disables them when people leave. The admin center shows identities, the portal shows configurations, and if someone asked "is identity managed in this tenant?" the answer would be yes.

But when the auditor asks why Priya Sharma still has access to the finance SharePoint site three months after transferring to engineering, when a service principal credential expires and a production integration fails at 2 AM, when a guest account from a vendor engagement that ended in 2023 still has active permissions to your project files — nobody can answer those questions from the admin center. The accounts exist. The access was granted. Nobody tracked whether the access still makes sense.

That's the gap this section makes concrete. Identity administration creates and manages accounts. Identity governance ensures that the access those accounts hold is justified, reviewed, and removed when it's no longer needed. Administration is the what — the accounts exist. Governance is the why — the accounts have this access for this reason, reviewed on this date, with this person accountable. Most tenants have administration. Most tenants lack governance. You'll see the difference in your own Graph API output in the next section.

Estimated time: 50 minutes.

ADMINISTRATION vs GOVERNANCE — THE IDENTITY GAP WHAT ADMINISTRATION SHOWS ✓ 810 user accounts — all active ✓ 214 groups — membership assigned ✓ 347 app registrations — credentials set Accounts exist. Access was granted. Looks managed. WHAT GOVERNANCE WOULD SHOW ✗ Why each identity has this access ✗ When access was last reviewed ✗ Who is accountable for each identity Governance: absent. Nobody knows if access is still justified. ? ADMINISTRATION Identities exist. Access was assigned. Query: Get-MgUser returns 810 accounts Returns who has access. Silent on whether they should. GOVERNANCE Identities exist + access justified + reviewed on schedule + accountable owner Returns who, why, when last reviewed, and by whom.

Figure IAM0.1 — Administration returns who has access. Governance documents why they have it, when it was last reviewed, and who is accountable. The gap between them is where access accumulates, audits fail, and compromises go undetected.

Your tenant's identity census — what the Graph API shows you

Open your M365 tenant. This PowerShell command queries every user through the Microsoft Graph, pulling the fields that separate administered identities from governed ones. You need the Microsoft.Graph.Users module and User.Read.All permission:

Connect-MgGraph -Scopes "User.Read.All"

Pull a summary of your tenant's identity landscape:

$users = Get-MgUser -All -Property id, displayName, userPrincipalName,
  userType, accountEnabled, createdDateTime, department, manager,
  employeeHireDate, signInActivity

$members = $users | Where-Object { $_.UserType -eq "Member" }
$guests = $users | Where-Object { $_.UserType -eq "Guest" }
$disabled = $users | Where-Object { $_.AccountEnabled -eq $false }

Write-Host "Total identities: $($users.Count)"
Write-Host "Members: $($members.Count)  Guests: $($guests.Count)  Disabled: $($disabled.Count)"

Here's what comes back from a typical production tenant — one where accounts are created, licenses assigned, and the admin center looks healthy:

Total identities: 847
Members: 810  Guests: 23  Disabled: 14

Eight hundred and forty-seven identities. That's administration — the accounts exist, the counts are known, the admin center shows a list. Now pull the governance data for those same identities:

$noDepartment = ($members | Where-Object { -not $_.Department }).Count
$noManager = ($members | Where-Object {
  -not (Get-MgUserManager -UserId $_.Id -ErrorAction SilentlyContinue)
}).Count
$noHireDate = ($members | Where-Object { -not $_.EmployeeHireDate }).Count

Write-Host "`nGovernance attribute coverage:"
Write-Host "  Missing department: $noDepartment / $($members.Count)"
Write-Host "  Missing manager:    $noManager / $($members.Count)"
Write-Host "  Missing hire date:  $noHireDate / $($members.Count)"
Governance attribute coverage:
  Missing department: 187 / 810
  Missing manager:    203 / 810
  Missing hire date:  649 / 810

Read this carefully. These aren't just missing fields. They're missing governance signals.

Missing department: 187 / 810 — 23% of member accounts have no department attribute. Dynamic group rules that assign access by department don't evaluate these identities. If you build a dynamic group for department -eq "Finance" to grant access to the finance SharePoint site, the 187 accounts with no department value silently fall outside the rule. They don't get access they should have, and nobody notices because the group technically "works."

Missing manager: 203 / 810 — 25% of accounts have no manager. Access reviews that route to the identity's manager can't assign a reviewer for these identities. When you create an access review for a security group and select "Manager of each user" as the reviewer, the review skips identities with no manager. Their access goes unreviewed. That's not a technical failure — the review completes successfully. It's a governance failure hidden inside a passing technical process.

Missing hire date: 649 / 810 — 80% of accounts have no employeeHireDate. Lifecycle workflows that trigger on hire date — pre-hire tasks, day-one automation, onboarding sequences — can't fire for those identities. The workflow exists, it's configured correctly, and it does nothing for four out of five employees because the attribute it depends on is empty.

Before you read further, look at your own output and answer this: if you built a lifecycle workflow that triggers 7 days before employeeHireDate to generate a Temporary Access Pass and send a welcome email, what percentage of your new hires would actually receive it?

At Northgate Engineering: The governance attribute coverage audit returns 187 members with no department (23%), 203 with no manager (25%), and 649 with no employeeHireDate (80%). Rachel Okafor runs these numbers after an auditor asks for evidence that access reviews cover all identities. The access review program she built six months ago uses manager-based reviewers. It works — for 75% of identities. The other 25% have been silently skipping every review cycle. The auditor flags this as a control gap: "Access review program does not provide complete coverage of member identities." The control exists. The governance has a hole. Administration can't see the hole. The query above can.

Tracing a group membership — where access hides

The census tells you the identity landscape. The next question is what access those identities actually hold. In most tenants, group membership is the primary access mechanism — security groups grant SharePoint access, M365 groups grant Teams membership, role-assignable groups grant Entra directory roles. Pick a user who's been in the organization for more than two years and trace their group memberships:

Entra Admin Center

IdentityUsersAll users → select a user who's been employed for 2+ years → Groups
Count the groups. Note how many relate to the user's current role vs previous roles. Note which groups you can identify by name and which have names like "SG-Project-Alpha" or "Team-Migration-2024" that suggest completed projects or past initiatives. The portal shows the membership list. It doesn't show when each membership was granted, who approved it, or whether it's been reviewed since.

To get the detail the portal doesn't show — group type, creation date, ownership state — query programmatically:

$targetUser = "priya.sharma@yourtenant.onmicrosoft.com"
$memberships = Get-MgUserMemberOf -UserId $targetUser -All

$memberships | ForEach-Object {
  $group = Get-MgGroup -GroupId $_.Id -Property displayName, groupTypes,
    securityEnabled, createdDateTime, description -ErrorAction SilentlyContinue
  if ($group) {
    $owners = (Get-MgGroupOwner -GroupId $_.Id -ErrorAction SilentlyContinue).Count
    [PSCustomObject]@{
      Name        = $group.DisplayName
      Type        = if ($group.GroupTypes -contains "Unified") { "M365" }
                    elseif ($group.SecurityEnabled) { "Security" }
                    else { "Distribution" }
      Created     = $group.CreatedDateTime.ToString("yyyy-MM-dd")
      Owners      = $owners
      Description = if ($group.Description) { $group.Description.Substring(0,
                    [Math]::Min(50, $group.Description.Length)) } else { "(NONE)" }
    }
  }
} | Sort-Object Created | Format-Table -AutoSize
Name                          Type      Created     Owners  Description
----                          ----      -------     ------  -----------
SG-All-Staff                  Security  2022-03-15  0       (NONE)
SG-Engineering                Security  2022-03-15  1       Engineering department access
SG-Finance-Readers            Security  2022-06-20  0       (NONE)
Team-Office-Migration-2023    M365      2023-01-09  0       (NONE)
SG-Project-Alpha              Security  2023-04-11  0       (NONE)
Team-SOC-L1                   M365      2023-08-14  2       SOC Level 1 analyst team
SG-SharePoint-HR-Site         Security  2023-11-02  1       HR site read access
SG-Defender-Alerts            Security  2024-01-15  1       Defender alert notification
Team-Vendor-Onboarding-2024   M365      2024-02-28  0       (NONE)
SG-Engineering-Leads          Security  2024-06-10  1       Engineering team leads
SG-Compliance-Training        Security  2024-09-01  0       (NONE)

Eleven groups. Study the output before reading the analysis.

SG-Finance-Readers — created in 2022. No owner. No description. This identity currently works in SOC operations but holds read access to the finance SharePoint site. When did the access start? Was there a project that required it? Did anyone review it when the identity moved from the team that needed finance data to one that doesn't? Administration can't answer any of those questions. The group exists. The membership was assigned. Nobody tracked the reason.

Team-Office-Migration-2023 and SG-Project-Alpha — project groups from completed initiatives. Both have zero owners and no description. The projects ended. The groups survive. Every identity in these groups still holds whatever access the group grants — SharePoint sites, Teams channels, possibly application permissions. That's access accumulation: access granted for a purpose that no longer exists, held by identities that no longer need it, in groups that nobody governs.

SG-SharePoint-HR-Site — HR site read access held by a SOC analyst. Is this deliberate? Maybe. SOC analysts sometimes need HR data during insider threat investigations. But the access was granted in November 2023 and has never been reviewed. If it was granted for a specific investigation, the investigation ended and the access should have been revoked. If it's standing access for the SOC role, it should be documented and reviewed quarterly.

Count the groups with zero owners. In the output above, it's five of eleven — 45% of this identity's group memberships are in ownerless groups. An ownerless group has no one accountable for membership decisions. When an access review runs, it can't route to the group owner because there isn't one. When someone needs to be removed, there's no owner to make the call. Ownerless groups are the most common governance gap in production tenants — they represent access that nobody is responsible for.

At Northgate Engineering: Priya Sharma transferred from the finance team to SOC operations 14 months ago. She still holds membership in SG-Finance-Readers, SG-Project-Alpha (a completed cross-department initiative), and Team-Office-Migration-2023 (a completed IT project). Her manager Tom Ashworth inherited the SOC team but was never notified of Priya's pre-existing group memberships. In an access review, Tom would approve access he knows about — the SOC groups — and either approve-all or skip the finance and project groups he doesn't recognize. That's three stale memberships surviving a role change, invisible to the current manager, granting access to data that Priya's current role doesn't require.

Stale identities — the access nobody revoked

What we see in 90% of tenants (and why it fails)

Accounts are created when people join. Accounts are sometimes disabled when people leave. Nobody tracks the gap in between — the accounts that exist but haven't been used in months, the guest accounts from completed engagements, the service accounts created by developers who left. These stale identities hold active access with no active human behind them. They're the first thing an attacker looks for because they're the last thing an admin monitors.

Group membership shows access accumulation for active identities. Stale identities show a different governance failure — accounts that should have been disabled or removed but weren't. Query your tenant for member accounts that haven't signed in for 90 days:

$staleDays = 90
$cutoff = (Get-Date).AddDays(-$staleDays).ToString("yyyy-MM-ddTHH:mm:ssZ")

$staleMembers = $members | Where-Object {
  $_.AccountEnabled -eq $true -and
  $_.SignInActivity.LastSignInDateTime -and
  $_.SignInActivity.LastSignInDateTime -lt $cutoff
}

Write-Host "Active members with no sign-in for $staleDays+ days: $($staleMembers.Count)"

$staleMembers | Select-Object displayName, userPrincipalName,
  @{N='LastSignIn'; E={$_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")}},
  department | Format-Table -AutoSize
Active members with no sign-in for 90+ days: 31

displayName          userPrincipalName                   LastSignIn  department
-----------          -----------------                   ----------  ----------
James Whitfield      j.whitfield@yourtenant.com          2025-11-14  Engineering
Maria Chen           m.chen@yourtenant.com               2025-12-03  (blank)
svc-legacy-app       svc-legacy@yourtenant.com           2025-10-22  (blank)
Sarah Blackwood      s.blackwood@yourtenant.com          2026-01-08  Marketing
...

Thirty-one enabled accounts with no interactive sign-in for 90+ days. Some are straightforward — employees on extended leave, accounts created for contractors whose engagement ended. Some are more interesting. svc-legacy-app is a service account with a user principal name format, not a service principal — someone created a regular user account for an application integration, probably because they didn't know how to create a service principal or the application didn't support modern authentication. That account still holds whatever access it was granted.

Maria Chen has no department attribute. No sign-in for over five months. Is she a former employee whose account was never disabled? An account created for testing that nobody cleaned up? The identity census from earlier tells you she's one of the 187 members with no department. Stale and ungoverned.

Now check guest accounts:

$staleGuests = $guests | Where-Object {
  $_.AccountEnabled -eq $true -and (
    (-not $_.SignInActivity.LastSignInDateTime) -or
    ($_.SignInActivity.LastSignInDateTime -lt $cutoff)
  )
}

Write-Host "Stale or never-signed-in guests: $($staleGuests.Count) / $($guests.Count)"

$staleGuests | Select-Object displayName, userPrincipalName, createdDateTime,
  @{N='LastSignIn'; E={
    if ($_.SignInActivity.LastSignInDateTime) {
      $_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")
    } else { "NEVER" }
  }} | Format-Table -AutoSize
Stale or never-signed-in guests: 14 / 23

displayName              userPrincipalName                         LastSignIn  createdDateTime
-----------              -----------------                         ----------  ---------------
Alex Rivera (Contoso)    alex.rivera_contoso.com#EXT#@...          NEVER       2023-06-14
Vendor-PenTest           vendor.pentest_external.com#EXT#@...      2024-03-12  2024-02-15
Sarah Kim (Partner)      sarah.kim_partner.com#EXT#@...            2024-08-20  2023-11-01
...

Fourteen of 23 guest accounts are stale or have never signed in. Alex Rivera was invited in June 2023 and has never authenticated — the invitation was sent, the account was created, but the guest never accepted or used it. That account still exists and may hold group memberships that grant access to resources. Vendor-PenTest was created for a penetration test in February 2024, last signed in during the engagement in March 2024, and has been idle since. The engagement ended. The account survived.

This is the pattern. Administration invited the guests. Governance would have set an expiry date, assigned a sponsor accountable for the account's lifecycle, and triggered a review when the engagement ended. Without governance, guest accounts accumulate. They're the identity equivalent of leaving the building's side door propped open after the delivery driver leaves.

At Northgate Engineering: Rachel Okafor runs the stale identity query and finds 31 active members with no sign-in for 90+ days and 14 stale guest accounts. She recognizes the vendor penetration test account — that engagement ended 14 months ago. She doesn't recognize 8 of the guest accounts at all. When she traces their group memberships, 3 of them still hold access to project-specific SharePoint sites with engineering documents. The accounts were invited, the access was granted, and nobody tracked the lifecycle. The stale guest query took 15 seconds. Finding and cleaning up those accounts takes a governance program.

Three questions that test any identity setting

Everything you've seen — the identity census, the group membership trace, the stale identity analysis — reduces to three questions. These are the lens you'll apply to every identity governance decision for the rest of the course. They work because they test for the three things that separate governance from administration: justified access, regular review, and clear accountability.

You can evaluate whether any identity setting in your tenant has governance behind it by asking these three questions. Every ADR you write from IAM1.7 onward answers all three.

Why does this identity have this access? Every group membership, every role assignment, every application permission represents an access decision. If the reasoning is documented — the access was requested for a specific purpose, approved by the resource owner, and granted with an expiry or review date — that's governance. If the access exists because someone added a user to a group during onboarding and nobody ever removed them, that's administration.

When was this access last reviewed? Access that was appropriate on the grant date may not be appropriate today. The finance analyst who transferred to engineering still has finance access. The project group from 2023 still grants access to project resources. The vendor guest account from a completed engagement still exists. Governance reviews access on a schedule — quarterly for privileged access, semi-annually for standard access, annually at minimum for everything. Administration doesn't review access at all. The question isn't whether a review has ever happened — it's whether the review cadence matches the risk.

Who is accountable for this identity? Every identity should have a human accountable for its lifecycle — its purpose, its access, its review, and its removal. For member accounts, the manager is the natural owner. For guest accounts, the sponsor who invited them. For service principals, the application owner. For groups, the group owner. When nobody is accountable, governance decisions don't get made. The identity persists. The access accumulates. The risk compounds. In the group membership trace above, 45% of groups had zero owners. Those groups represent access that nobody is responsible for.

Run these three questions against your own tenant now. Pick one identity — a user who's been with the organization for more than two years, a guest from a completed project, or a service account. Trace their group memberships. For each group, ask: why does this identity have this membership, when was it last reviewed, and who is accountable? Count how many have documented answers for all three.


Reusable script — the diagnostic commands from this section assembled for operational use:

# IAM0.1 — Identity Governance Diagnostic
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All",
  "GroupMember.Read.All", "AuditLog.Read.All"

# 1. Identity census
$users = Get-MgUser -All -Property id, displayName, userPrincipalName,
  userType, accountEnabled, createdDateTime, department, manager,
  employeeHireDate, signInActivity

$members = $users | Where-Object { $_.UserType -eq "Member" }
$guests = $users | Where-Object { $_.UserType -eq "Guest" }
$disabled = $users | Where-Object { $_.AccountEnabled -eq $false }

Write-Host "=== IDENTITY CENSUS ==="
Write-Host "Total: $($users.Count) | Members: $($members.Count) | Guests: $($guests.Count) | Disabled: $($disabled.Count)"

# 2. Governance attribute coverage
$noDept = ($members | Where-Object { -not $_.Department }).Count
$noMgr = ($members | Where-Object {
  -not (Get-MgUserManager -UserId $_.Id -ErrorAction SilentlyContinue)
}).Count
$noHire = ($members | Where-Object { -not $_.EmployeeHireDate }).Count

Write-Host "`n=== GOVERNANCE ATTRIBUTE COVERAGE ==="
Write-Host "Missing department: $noDept / $($members.Count) ($([math]::Round($noDept/$members.Count*100))%)"
Write-Host "Missing manager:    $noMgr / $($members.Count) ($([math]::Round($noMgr/$members.Count*100))%)"
Write-Host "Missing hire date:  $noHire / $($members.Count) ($([math]::Round($noHire/$members.Count*100))%)"

# 3. Stale member identities (90+ days no sign-in)
$staleDays = 90
$cutoff = (Get-Date).AddDays(-$staleDays).ToString("yyyy-MM-ddTHH:mm:ssZ")

$staleMembers = $members | Where-Object {
  $_.AccountEnabled -eq $true -and
  $_.SignInActivity.LastSignInDateTime -and
  $_.SignInActivity.LastSignInDateTime -lt $cutoff
}

Write-Host "`n=== STALE MEMBER IDENTITIES ($staleDays+ days) ==="
Write-Host "Count: $($staleMembers.Count)"
$staleMembers | Select-Object displayName, userPrincipalName,
  @{N='LastSignIn'; E={$_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")}},
  department | Format-Table -AutoSize

# 4. Stale or never-signed-in guest identities
$staleGuests = $guests | Where-Object {
  $_.AccountEnabled -eq $true -and (
    (-not $_.SignInActivity.LastSignInDateTime) -or
    ($_.SignInActivity.LastSignInDateTime -lt $cutoff)
  )
}

Write-Host "=== STALE GUEST IDENTITIES ==="
Write-Host "Count: $($staleGuests.Count) / $($guests.Count) total guests"
$staleGuests | Select-Object displayName, createdDateTime,
  @{N='LastSignIn'; E={
    if ($_.SignInActivity.LastSignInDateTime) {
      $_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")
    } else { "NEVER" }
  }} | Format-Table -AutoSize

# 5. User group membership trace (replace UPN)
$targetUser = "admin@yourtenant.onmicrosoft.com"
Write-Host "`n=== GROUP MEMBERSHIP TRACE: $targetUser ==="
$memberships = Get-MgUserMemberOf -UserId $targetUser -All

$memberships | ForEach-Object {
  $group = Get-MgGroup -GroupId $_.Id -Property displayName, groupTypes,
    securityEnabled, createdDateTime, description -ErrorAction SilentlyContinue
  if ($group) {
    $owners = (Get-MgGroupOwner -GroupId $_.Id -ErrorAction SilentlyContinue).Count
    [PSCustomObject]@{
      Name    = $group.DisplayName
      Type    = if ($group.GroupTypes -contains "Unified") { "M365" }
                elseif ($group.SecurityEnabled) { "Security" }
                else { "Distribution" }
      Created = $group.CreatedDateTime.ToString("yyyy-MM-dd")
      Owners  = $owners
      Desc    = if ($group.Description) {
                  $group.Description.Substring(0, [Math]::Min(40, $group.Description.Length))
                } else { "(NONE)" }
    }
  }
} | Sort-Object Created | Format-Table -AutoSize

For each identity with no department, note it. For each stale guest, note it. For each ownerless group, note it. This inventory is your starting point — by IAM14, every entry will have documented governance behind it.

Decision-point simulation

Scenario 1. Your identity census reveals 810 member accounts, 89 guest accounts, and 347 app registrations. The CISO asks: "Are we governed?" You ran a governance attribute check and found 40% of members are missing department or job title. What's your answer, and how do you frame the gap without creating panic?

The answer is "partially — and now we can measure it." The 40% gap is a data quality issue, not a security emergency. Frame it as: "We administer 810 identities. We govern the ones with complete attributes — currently 60%. The 40% gap means lifecycle workflows, dynamic groups, and access reviews can't cover those users. The remediation is attribute completion, not new tooling." The CISO needs a number, a cause, and a plan — not a list of failures.

Scenario 2. Phil Greaves (IT Director) argues that identity governance is unnecessary overhead because "we've never had an identity-related breach." How do you respond?

The absence of a detected breach is not evidence of governance. Run the permission creep query from this section — if users who changed departments 18 months ago still hold their old access, the attack surface exists whether or not it's been exploited. Show the data: how many users have access they shouldn't? How many guest accounts are stale? How many app registrations have expired credentials? Governance isn't breach prevention alone — it's audit readiness, operational efficiency, and reducing the blast radius when a breach does occur.

Scenario 3. You discover that 67 of 89 guest accounts have no sponsor. Elena Petrova (GRC) says the ISO 27001 auditor will flag this. Phil says assigning sponsors retroactively is busywork. Who's right, and what do you recommend?

Both have a point. Retroactive sponsor assignment without context is busywork — nobody knows why half these guests were invited. But the auditor will flag unsponsored guests as a control gap (ISO 27001 A.5.18). The pragmatic path: assign the original inviter as sponsor where the invitation record exists. For guests where the inviter has left or can't be identified, assign the IT Director as temporary sponsor with a 30-day review deadline. Guests that nobody claims after 30 days get disabled. This satisfies the audit requirement while identifying which guests are genuinely needed.

Next

IAM0.2 — The Identity Lifecycle in Your Tenant. You've seen the governance gap — identities administered but not governed, access granted but not reviewed. IAM0.2 traces the five-stage identity lifecycle through your own Graph API data: creation (where did this identity come from?), assignment (how did it get this access?), governance (was the access ever reviewed?), monitoring (when did it last sign in?), and removal (what happens when it shouldn't exist anymore?). The mover problem — the identity that changes roles and keeps old access — gets its first detailed treatment.

You're reading the free modules of Identity and Access Management in Microsoft 365

The full course continues with advanced topics, production detection rules, worked investigation scenarios, and deployable artifacts.

View Pricing See Full Syllabus