In this module

IAM1.2 The Identity Data Model

8 hours · Module 1 · Free
What you already know

IAM1.1 examined the five identity types as governance objects. This section goes deeper into the data model that drives governance automation — the specific user properties that lifecycle workflows, dynamic groups, access reviews, and entitlement management depend on. You'll query every governance-critical attribute in your tenant, understand what each one enables, and build an attribute dependency map showing which governance mechanisms break when specific attributes are missing.

The attributes that governance depends on

The Graph API user resource has over 100 properties. Most are administrative — displayName, mail, businessPhones. A subset of those properties drive governance automation. When they're populated, lifecycle workflows fire, dynamic groups evaluate correctly, access reviews route to the right reviewers, and entitlement management can scope access packages by organizational criteria. When they're missing, those mechanisms fail silently — the workflow doesn't trigger, the dynamic group skips the user, the review has no reviewer, and the access package doesn't auto-assign.

This section examines twelve governance-critical attributes across four categories: lifecycle attributes (when the identity joined and when it should leave), organizational attributes (where the identity fits in the structure), activity attributes (how recently the identity authenticated), and extended attributes (custom governance metadata). For each attribute, you'll understand what it enables, what breaks when it's missing, how to query it, and how to interpret the output.

Estimated time: 55 minutes.

ATTRIBUTE → GOVERNANCE MECHANISM DEPENDENCIES ATTRIBUTES employeeHireDate employeeLeaveDateTime department employeeType manager signInActivity customSecurityAttributes employeeOrgData lastPasswordChange GOVERNANCE MECHANISMS Lifecycle workflow — joiner (M2) Lifecycle workflow — leaver (M2) Dynamic group membership (M3) Workflow scoping + auto-assignment (M2/M4) Manager-based access reviews (M5) Stale identity detection (M12) Advanced CA + dynamic rules (M3/M6) Organizational scoping (M4/M8) Credential age monitoring (M12) ── Critical dependency ╌╌ Supporting attribute

Figure IAM1.2 — Each governance mechanism depends on specific attributes. Solid lines show critical dependencies (mechanism fails without the attribute). Dashed lines show supporting attributes (mechanism works but with reduced capability). Data quality from IAM1.3 determines how many of these connections are functional.

Lifecycle attributes — when the identity joined and when it leaves

Three attributes drive the time-based lifecycle: employeeHireDate, employeeLeaveDateTime, and createdDateTime. Together, they define the identity's temporal existence — when it was supposed to arrive, when it actually arrived, and when it should leave.

Query all three for your NE personas:

Connect-MgGraph -Scopes "User.Read.All", "User-LifeCycleInfo.Read.All",
  "AuditLog.Read.All"

$personas = Get-MgUser -All -Property displayName, employeeHireDate,
  employeeLeaveDateTime, createdDateTime, accountEnabled |
  Where-Object { $_.AccountEnabled -eq $true }

$personas | Select-Object displayName,
  @{N='HireDate'; E={
    if ($_.EmployeeHireDate) { $_.EmployeeHireDate.ToString("yyyy-MM-dd") }
    else { "MISSING" }
  }},
  @{N='LeaveDate'; E={
    if ($_.EmployeeLeaveDateTime) { $_.EmployeeLeaveDateTime.ToString("yyyy-MM-dd") }
    else { "NOT SET" }
  }},
  @{N='Created'; E={ $_.CreatedDateTime.ToString("yyyy-MM-dd") }},
  @{N='HireLag'; E={
    if ($_.EmployeeHireDate) {
      ($_.CreatedDateTime - $_.EmployeeHireDate).Days
    } else { "N/A" }
  }} | Format-Table -AutoSize
displayName      HireDate    LeaveDate  Created     HireLag
-----------      --------    ---------  -------     -------
Rachel Okafor    2024-11-15  NOT SET    2026-05-11  543
Tom Ashworth     2023-08-01  NOT SET    2026-05-11  1015
Priya Sharma     2022-06-15  NOT SET    2026-05-11  1427
David Chen       2021-09-01  NOT SET    2026-05-11  1714
Anna Kowalski    2023-06-01  NOT SET    2026-05-11  1076
Fatima Al-Rashid 2024-01-15  NOT SET    2026-05-11  848
Phil Greaves     2022-03-01  NOT SET    2026-05-11  1533
James Whitfield  MISSING     NOT SET    2026-05-11  N/A
Sarah Blackwood  MISSING     NOT SET    2026-05-11  N/A
Mark Taylor      MISSING     NOT SET    2026-05-11  N/A
Lisa Okonkwo     MISSING     NOT SET    2026-05-11  N/A
Ben Hughes       MISSING     NOT SET    2026-05-11  N/A
Chris Morrison   MISSING     NOT SET    2026-05-11  N/A
Elena Petrova    2024-03-01  NOT SET    2026-05-11  803
Marcus Webb      2023-01-10  NOT SET    2026-05-11  1218

employeeHireDate — the attribute lifecycle workflows use to trigger pre-hire and onboarding tasks. A joiner workflow configured to fire 7 days before hire date generates a Temporary Access Pass, sends a welcome email, and assigns the onboarding group. Without this attribute, the workflow can't calculate the trigger date and the identity doesn't receive onboarding automation. In the output above, 6 of 15 personas are missing hire dates — those identities are invisible to hire-date-triggered workflows.

The HireLag column reveals something else. The personas were all created on the same day (the lab setup), but their hire dates span years. In a production tenant, the lag between employeeHireDate and createdDateTime tells you how far in advance accounts are provisioned. A lag of 0 means the account was created on the hire date — no pre-hire automation was possible. A lag of -7 (account created 7 days before hire) means pre-hire workflows had time to run. A lag of +30 (account created 30 days after hire) means the employee started work a month before their account existed.

employeeLeaveDateTime — the trigger for leaver workflows. When this attribute is set to a future date, lifecycle workflows can schedule pre-departure tasks (disable account, revoke sessions, remove group memberships) in advance. It requires the User-LifeCycleInfo.Read.All permission to read and User-LifeCycleInfo.ReadWrite.All to write — a deliberate permission gate because leave dates are sensitive HR data. In the lab output, no personas have leave dates set. In a production tenant, this attribute is typically populated by HR-driven provisioning when a termination is processed.

createdDateTime — the timestamp of when the user object was actually created in Entra ID. This isn't a governance-triggering attribute, but it's essential for audit: it tells you when the identity entered the directory, which you can compare against hire date to measure provisioning timeliness and against signInActivity to detect accounts that were never used.

Entra Admin Center

IdentityUsersAll users → select a user → PropertiesJob info
The portal shows Employee hire date and Employee leave date/time in the Job info section. Note that employeeHireDate cannot be updated through app-only (application) permissions — it requires delegated permissions with a signed-in user. This is a Microsoft Graph constraint documented in the Update user API: "The following properties cannot be updated by an app with only application permissions: aboutMe, birthday, employeeHireDate, interests, mySite, pastProjects, responsibilities, schools, and skills." If you're automating attribute population through a service principal, you'll need a workaround — either delegated permission flow or an alternative attribute for workflow triggering.

Organizational attributes — where the identity fits

Five attributes define the identity's position in the organizational structure: department, jobTitle, manager, employeeType, and employeeOrgData (which contains division and costCenter).

Entra Admin Center

IdentityUsersAll users → select a user → PropertiesEdit propertiesJob info
The Job info section shows Department, Job title, Employee type, and Employee ID. Note which fields are populated and which are blank. The Employee type field accepts free-text values — "Employee", "Contractor", "Intern", or any custom value. This is the field lifecycle workflows use for scoping, and the admin center doesn't prompt for it during user creation. To see the manager assignment, check Manager in the left nav or the Organization tab in Properties.

$orgData = Get-MgUser -All -Property displayName, department, jobTitle,
  employeeType, employeeOrgData, companyName |
  Where-Object { $_.AccountEnabled -eq $true }

$orgData | Select-Object displayName, department, jobTitle,
  @{N='EmpType'; E={ if ($_.EmployeeType) { $_.EmployeeType } else { "MISSING" }}},
  @{N='Division'; E={
    if ($_.EmployeeOrgData.Division) { $_.EmployeeOrgData.Division }
    else { "MISSING" }
  }},
  @{N='CostCenter'; E={
    if ($_.EmployeeOrgData.CostCenter) { $_.EmployeeOrgData.CostCenter }
    else { "MISSING" }
  }} | Format-Table -AutoSize
displayName      department  jobTitle           EmpType  Division  CostCenter
-----------      ----------  --------           -------  --------  ----------
Rachel Okafor    Security    CISO               MISSING  MISSING   MISSING
Tom Ashworth     Security    SOC Analyst L1     MISSING  MISSING   MISSING
Priya Sharma     Security    SOC Analyst L1     MISSING  MISSING   MISSING
Phil Greaves     IT          IT Director        MISSING  MISSING   MISSING
Sarah Blackwood  (blank)     Marketing Lead     MISSING  MISSING   MISSING
Mark Taylor      (blank)     (blank)            MISSING  MISSING   MISSING
...

Each attribute drives specific governance mechanisms:

department — the primary scoping attribute for dynamic groups. A dynamic membership rule like user.department -eq "Security" automatically adds every identity with department: Security to the group. When an identity's department changes (Priya transferring from Finance to Security), the dynamic group adjusts automatically — removing her from the Finance group and adding her to the Security group. This is the mechanism that solves the mover problem from IAM0.2. But it only works when department is populated. Sarah Blackwood and Mark Taylor, with no department value, fall outside every department-scoped dynamic group.

manager — the routing attribute for access reviews and lifecycle workflow notifications. A manager-based access review sends each identity's access to their manager for certification. An identity with no manager has no reviewer — their access goes unreviewed. Query the manager coverage:

$managerCoverage = $orgData | ForEach-Object {
  $mgr = Get-MgUserManager -UserId $_.Id -ErrorAction SilentlyContinue
  [PSCustomObject]@{
    Name       = $_.DisplayName
    HasManager = [bool]$mgr
    Manager    = if ($mgr) { $mgr.AdditionalProperties.displayName } else { "NONE" }
  }
}

$managerCoverage | Format-Table -AutoSize
$noManager = ($managerCoverage | Where-Object { -not $_.HasManager }).Count
Write-Host "`nIdentities without manager: $noManager / $($managerCoverage.Count)"

employeeType — distinguishes employees from contractors, interns, vendors, and other categories. Lifecycle workflows use this as a scoping condition — you can build a joiner workflow that fires only for employeeType -eq "Employee" and a different workflow for employeeType -eq "Contractor" with a shorter access duration. Dynamic group rules can scope by employeeType. Entitlement management can restrict access packages by employee type. In the lab, every persona has employeeType: MISSING — this is the most commonly absent governance attribute in production tenants because it requires deliberate population by HR or admin.

employeeOrgData — a complex property containing division and costCenter. These are the organizational structure attributes that go beyond department — useful for large organizations with matrix structures where department alone doesn't capture reporting relationships. Division-scoped dynamic groups, cost-center-based entitlement management, and business-unit-level access reviews all depend on these attributes.

Query the employeeOrgData coverage for a more complete picture of organizational scoping potential:

$orgComplete = $orgData | ForEach-Object {
  [PSCustomObject]@{
    Name       = $_.DisplayName
    Department = if ($_.Department) { $_.Department } else { "—" }
    Division   = if ($_.EmployeeOrgData.Division) { $_.EmployeeOrgData.Division } else { "—" }
    CostCenter = if ($_.EmployeeOrgData.CostCenter) { $_.EmployeeOrgData.CostCenter } else { "—" }
    Company    = if ($_.CompanyName) { $_.CompanyName } else { "—" }
  }
}
$orgComplete | Format-Table -AutoSize

$hasDivision = ($orgData | Where-Object { $_.EmployeeOrgData.Division }).Count
$hasCostCenter = ($orgData | Where-Object { $_.EmployeeOrgData.CostCenter }).Count
$hasCompany = ($orgData | Where-Object { $_.CompanyName }).Count

Write-Host "`nOrganizational scoping coverage:"
Write-Host "  division:    $hasDivision / $total ($([math]::Round($hasDivision/$total*100))%)"
Write-Host "  costCenter:  $hasCostCenter / $total ($([math]::Round($hasCostCenter/$total*100))%)"
Write-Host "  companyName: $hasCompany / $total ($([math]::Round($hasCompany/$total*100))%)"

In most tenants, division and costCenter coverage is near zero. These fields require deliberate population — they're not prompted in the standard admin center user creation form and they're not populated by basic Entra Connect sync configurations without explicit attribute mapping. The governance implication: if your organization needs to scope access packages or reviews by business unit (division) or financial unit (cost center), you need to either configure HR-driven provisioning to populate these fields or add them to your Entra Connect attribute mapping. The companyName field is more commonly populated in multi-subsidiary environments where identities from different legal entities coexist in one tenant.

At Northgate Engineering: Phil Greaves populates department and jobTitle when creating accounts because the admin center prompts for them. He doesn't populate employeeType because the field isn't required and he doesn't know it drives lifecycle workflow scoping. He's never heard of employeeOrgData — the division and costCenter sub-properties don't appear in the admin center's standard user creation form. The gap isn't negligence. It's visibility — the admin center shows the fields it considers important for administration. It doesn't highlight the fields that governance depends on.

Activity attributes — how recently the identity authenticated

Entra Admin Center

IdentityUsersAll users → select a user → Sign-in logs
The sign-in logs show individual authentication events with timestamps, status (success/failure), client app, resource accessed, and Conditional Access evaluation results. For a quick staleness check, look at the most recent entry's date. If there are no entries, the account has either never been used or the sign-in data has aged out of the log retention period (default 30 days for the interactive view, though signInActivity on the user object retains the last timestamp indefinitely). The portal logs show detail per event. The signInActivity property on the user object — queried through PowerShell — gives you the summary timestamps you need for governance decisions.

The signInActivity property contains three governance-critical timestamps, each serving a different staleness detection purpose:

$activityData = Get-MgUser -All -Property displayName, signInActivity,
  lastPasswordChangeDateTime |
  Where-Object { $_.AccountEnabled -eq $true -and $_.UserType -eq "Member" }

$activityData | Select-Object displayName,
  @{N='LastInteractive'; E={
    if ($_.SignInActivity.LastSignInDateTime) {
      $_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd HH:mm")
    } else { "NEVER" }
  }},
  @{N='LastSuccessful'; E={
    if ($_.SignInActivity.LastSuccessfulSignInDateTime) {
      $_.SignInActivity.LastSuccessfulSignInDateTime.ToString("yyyy-MM-dd HH:mm")
    } else { "NEVER" }
  }},
  @{N='LastNonInteractive'; E={
    if ($_.SignInActivity.LastNonInteractiveSignInDateTime) {
      $_.SignInActivity.LastNonInteractiveSignInDateTime.ToString("yyyy-MM-dd HH:mm")
    } else { "NEVER" }
  }},
  @{N='PwdChanged'; E={
    if ($_.LastPasswordChangeDateTime) {
      $_.LastPasswordChangeDateTime.ToString("yyyy-MM-dd")
    } else { "NEVER" }
  }} | Format-Table -AutoSize

LastSignInDateTime — the most recent interactive sign-in attempt. The user entered credentials (password, FIDO2 key, Windows Hello) and completed authentication. An attempt doesn't mean success — the user may have failed MFA, been blocked by Conditional Access, or entered the wrong password. Use this to detect when someone last tried to access the tenant, even if they failed.

LastSuccessfulSignInDateTime — the most recent successful sign-in, whether interactive or non-interactive. Added to the v1.0 API in December 2023, this is the most reliable staleness indicator. It captures both human sign-ins (interactive) and application-driven token refreshes (non-interactive), and only counts successful authentications. An identity with no successful sign-in for 90 days is a governance finding regardless of whether the last attempt was interactive.

LastNonInteractiveSignInDateTime — the most recent background authentication event. Token refreshes, background app activity, and SSO token renewals all generate non-interactive sign-ins. An identity with a recent non-interactive sign-in but a stale interactive sign-in is still "active" in the technical sense — applications are using their tokens — but the human may not be actively working. The governance interpretation depends on context: a service account with only non-interactive sign-ins is behaving correctly, while a human account with only non-interactive sign-ins for 60 days may indicate the person has left but background services are still refreshing tokens.

The signInActivity property is stored outside Entra ID's main data store. You must explicitly request it with $select or -Property — it's not returned by default user queries. It also requires AuditLog.Read.All permission. If your query returns empty signInActivity, verify both the $select parameter and the permission scope.

There's a practical constraint with signInActivity in bulk queries: Microsoft limits the $top parameter to 120 users per page when signInActivity is included in the $select. The Get-MgUser -All cmdlet handles pagination automatically, but if you're writing direct Graph API calls, you'll need to handle the @odata.nextLink pagination manually. In tenants with thousands of users, the full signInActivity pull takes several minutes. Plan your diagnostic scripts accordingly — run the sign-in activity audit during a scheduled maintenance window, not interactively while troubleshooting.

lastPasswordChangeDateTime — when the identity last changed their password. For governance, this attribute identifies identities using the same credential for extended periods. In passwordless environments (FIDO2, Windows Hello for Business), this attribute becomes less meaningful because the password may never be the primary authentication method. In environments still relying on password-based authentication, a password age of 12+ months combined with no MFA registration is a compound governance risk.

Extended attributes — custom governance metadata

Standard attributes cover the basics. For governance scenarios that require organization-specific classification — risk tier, data handling clearance, project assignment, compliance training completion — Entra ID provides two extension mechanisms.

Custom security attributes are the governance-grade extension. They support fine-grained RBAC (only users with the Attribute Assignment Administrator role can read or write them), appear on both users and service principals, and can be used in Conditional Access policy conditions and dynamic group membership rules. They require the CustomSecAttributeAssignment.Read.All permission to read.

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

# Check if any custom security attribute sets are defined
$attrSets = Invoke-MgGraphRequest -Method GET `
  -Uri "https://graph.microsoft.com/v1.0/directory/customSecurityAttributeDefinitions" `
  -OutputType PSObject

Write-Host "Custom security attribute definitions: $($attrSets.value.Count)"
$attrSets.value | Select-Object attributeSet, name, type, isSearchable,
  isCollection | Format-Table -AutoSize

In a new developer tenant, this returns zero definitions — you haven't created any custom security attributes yet. In a production tenant, you might find attributes like RiskTier (High/Medium/Low), DataClearance (Confidential/Internal/Public), or ComplianceTraining (Complete/InProgress/Overdue). Module 3 uses custom security attributes for advanced dynamic group rules when standard attributes don't capture the governance classification you need.

The governance value of custom security attributes is that they extend the data model with organization-specific metadata that standard attributes can't represent. The governance cost is that they require population — another attribute that must be set correctly for the governance mechanism to work.

Extension attributes (onPremisesExtensionAttributes.extensionAttribute1 through extensionAttribute15) are the legacy mechanism, synced from on-premises Active Directory. They're string fields with no schema enforcement, no RBAC protection, and no meaningful names. They're widely used in production because they were available before custom security attributes existed. For new governance programs, custom security attributes are the better choice. For environments with existing extension attribute usage, you'll need to audit what's already there before designing the governance data model.

The attribute dependency map

Every governance mechanism in this course depends on specific attributes. When those attributes are missing, the mechanism fails — silently, without error, producing no output instead of wrong output. Understanding these dependencies before you build is essential. The alternative is discovering them after deployment when a lifecycle workflow runs successfully for 20% of your workforce and silently skips the other 80%.

Lifecycle workflows (joiner) depend on employeeHireDate. The workflow evaluates a time-based trigger — "7 days before hire date, generate a Temporary Access Pass and send a welcome email." Without a hire date, the trigger has no anchor. The workflow doesn't error. It doesn't skip with a warning. The identity simply doesn't appear in the workflow's evaluation scope. If 80% of your members are missing employeeHireDate, 80% of new hires won't receive onboarding automation. You'll discover this when a new employee starts work and has no account, no TAP, and no welcome email — and the workflow logs show successful execution for the 20% it could evaluate.

Lifecycle workflows (leaver) depend on employeeLeaveDateTime. The pattern is the same — a time-based trigger with no fallback. Without a leave date, the leaver workflow never fires. The departing employee's account stays enabled, sessions stay active, and group memberships persist until someone manually disables the account. The workflow exists. It works perfectly for identities with leave dates. It's invisible to identities without them.

Lifecycle workflow scoping uses employeeType and department to determine which workflow applies to which identities. A workflow scoped to employeeType -eq "Contractor" with a 90-day access duration only evaluates identities where employeeType is populated and equals "Contractor." If your contractors don't have employeeType set, the contractor-specific workflow never fires and they receive the same onboarding as permanent employees — or no onboarding at all.

Dynamic group membership evaluates rules against department, employeeType, companyName, jobTitle, and custom security attributes. A rule like user.department -eq "Finance" works for every identity with a department value and silently excludes every identity without one. The group looks correct — it contains finance users. It doesn't contain finance users with empty department fields. The governance gap is invisible from the group membership list.

Manager-based access reviews route each identity's access to their manager for certification. Identities without a manager assignment have no reviewer. Depending on the review configuration, they're either skipped (the default) or routed to a fallback reviewer (if configured). If 25% of your members have no manager, 25% of access goes unreviewed in every review cycle.

Entitlement management auto-assignment grants access packages automatically when identities meet attribute criteria. A policy that auto-assigns the "Finance Analyst" package when department -eq "Finance" AND employeeType -eq "Employee" requires both attributes. An employee in finance with no employeeType doesn't receive the package. They email IT instead, and Phil creates a manual group assignment — exactly the ungoverned process entitlement management was designed to replace.

Decision point

You have the attribute dependency map and the coverage data from your audit. Before moving to IAM1.3, answer this: given your tenant's current attribute coverage, which governance mechanisms can you deploy today and which require data remediation first?

If your tenant has 80% department coverage, dynamic groups by department will work for most identities. If employeeHireDate coverage is 20%, lifecycle workflows can't launch until data quality improves. If manager coverage is 75%, access reviews will cover three-quarters of your workforce — useful, but the gap needs documentation.

The answer to this question — what you can deploy now vs what requires data remediation — is the prioritization decision that drives IAM1.3.

Save the dependency map. When you build each governance mechanism in Modules 2 through 12, the first step is verifying that the required attributes are populated for the identities in scope. IAM1.3 measures the coverage gap across all governance-critical attributes and defines the remediation strategies for closing it.

At Northgate Engineering: Elena Petrova produces the attribute dependency map for Rachel Okafor and highlights the compound failures: lifecycle workflows require employeeHireDate (40% coverage) AND employeeType (0% coverage) for scoped joiner automation. Dynamic groups require department (80% coverage) but don't have employeeType for employment-category scoping. Manager-based access reviews require manager (75% coverage), leaving 25% of identities unreviewed. The governance program can't launch until the data quality reaches a minimum threshold. That threshold — and the remediation plan to reach it — is the focus of IAM1.3.


Reusable script — the data model audit from this section:

# IAM1.2 — Identity Data Model Audit
Connect-MgGraph -Scopes "User.Read.All", "User-LifeCycleInfo.Read.All",
  "AuditLog.Read.All", "CustomSecAttributeAssignment.Read.All"

$users = Get-MgUser -All -Property displayName, accountEnabled, userType,
  department, jobTitle, employeeHireDate, employeeLeaveDateTime,
  employeeType, employeeOrgData, companyName, onPremisesSyncEnabled,
  createdDateTime, signInActivity, lastPasswordChangeDateTime

$active = $users | Where-Object {
  $_.AccountEnabled -eq $true -and $_.UserType -eq "Member"
}
$total = $active.Count

Write-Host "=== IDENTITY DATA MODEL AUDIT ($total active members) ==="

# Lifecycle attributes
$hireDate = ($active | Where-Object { $_.EmployeeHireDate }).Count
$leaveDate = ($active | Where-Object { $_.EmployeeLeaveDateTime }).Count
Write-Host "`nLIFECYCLE ATTRIBUTES"
Write-Host "  employeeHireDate:      $hireDate ($([math]::Round($hireDate/$total*100))%)"
Write-Host "  employeeLeaveDateTime: $leaveDate ($([math]::Round($leaveDate/$total*100))%)"

# Organizational attributes
$dept = ($active | Where-Object { $_.Department }).Count
$title = ($active | Where-Object { $_.JobTitle }).Count
$empType = ($active | Where-Object { $_.EmployeeType }).Count
$division = ($active | Where-Object { $_.EmployeeOrgData.Division }).Count

Write-Host "`nORGANIZATIONAL ATTRIBUTES"
Write-Host "  department:            $dept ($([math]::Round($dept/$total*100))%)"
Write-Host "  jobTitle:              $title ($([math]::Round($title/$total*100))%)"
Write-Host "  employeeType:          $empType ($([math]::Round($empType/$total*100))%)"
Write-Host "  division:              $division ($([math]::Round($division/$total*100))%)"

# Manager coverage
$withMgr = 0
foreach ($u in $active) {
  if (Get-MgUserManager -UserId $u.Id -ErrorAction SilentlyContinue) {
    $withMgr++
  }
}
Write-Host "  manager:               $withMgr ($([math]::Round($withMgr/$total*100))%)"

# Activity attributes
$hasSignIn = ($active | Where-Object {
  $_.SignInActivity.LastSuccessfulSignInDateTime
}).Count
$hasPwdChange = ($active | Where-Object {
  $_.LastPasswordChangeDateTime
}).Count
Write-Host "`nACTIVITY ATTRIBUTES"
Write-Host "  signInActivity:        $hasSignIn ($([math]::Round($hasSignIn/$total*100))%)"
Write-Host "  lastPwdChange:         $hasPwdChange ($([math]::Round($hasPwdChange/$total*100))%)"

# Identity source
$cloud = ($active | Where-Object { -not $_.OnPremisesSyncEnabled }).Count
$synced = ($active | Where-Object { $_.OnPremisesSyncEnabled }).Count
Write-Host "`nIDENTITY SOURCE"
Write-Host "  Cloud-native:          $cloud"
Write-Host "  Synced from AD:        $synced"

# Custom security attributes
$csaDefs = (Invoke-MgGraphRequest -Method GET `
  -Uri "https://graph.microsoft.com/v1.0/directory/customSecurityAttributeDefinitions" `
  -OutputType PSObject -ErrorAction SilentlyContinue).value.Count
Write-Host "`nEXTENDED ATTRIBUTES"
Write-Host "  Custom security attrs: $csaDefs definitions"
What we see in 90% of tenants (and why it fails)

The admin creates 50 users through the portal. Each user has a display name, a UPN, and a password. The department field is filled for 30 of the 50 because the admin remembered to set it. The other 20 get added "later" — which means never. When someone builds a dynamic group for the Finance department, it contains 18 members instead of 23 because 5 Finance users have a blank department field. Nobody notices because the group looks populated. The downstream access assignments, license allocations, and Conditional Access targeting all miss those 5 users. The root cause isn't the dynamic group rule — it's the attribute gap that nobody measured.

Decision-point simulation

Scenario 1. Your data model audit reveals that employeeType is populated for 95% of users — but 60% have the value "Employee" and the remaining 35% have variations: "FTE", "Full-Time", "Permanent", "Regular." Dynamic group rules targeting employeeType -eq "Employee" miss 35% of permanent staff. What's the remediation approach?

Standardize the attribute values. Define the canonical values in ADR-IAM2-002 (Employee, Contractor, Intern, Vendor) and run a bulk update script to normalize existing data. The 35% with non-standard values get mapped: "FTE" → Employee, "Full-Time" → Employee, "Permanent" → Employee, "Regular" → Employee. Then enforce the standard at creation time through the New-GovernedUser function that rejects non-canonical values. This is a one-time remediation with a permanent prevention control.

Scenario 2. The HR system populates department using codes (ENG-001, FIN-002, MKT-003) but your dynamic group rules use human-readable names (Engineering, Finance, Marketing). The mapping breaks every time HR adds a new department code. Where should the translation live?

Not in the dynamic group rules — those should reference the attribute value directly. The translation belongs in the provisioning pipeline. If you're using Entra Connect, the attribute mapping in the sync configuration transforms HR codes to readable names before they reach Entra ID. If you're using API-driven provisioning, the transformation logic lives in the provisioning script or Logic App. The principle: transform once at ingestion, consume the clean value everywhere downstream. Module 10 covers provisioning attribute mapping.

Scenario 3. A Graph API query returns signInActivity.lastSignInDateTime as null for 120 users. Your manager wants to classify all 120 as inactive. Why is that potentially wrong?

The signInActivity property requires an Entra ID P1 or P2 license AND the sign-in must have occurred after Microsoft started recording the property (late 2019). Null can mean: never signed in, signed in before tracking started, or the property wasn't requested with $select in the query. Cross-reference with createdDateTime — a user created in 2024 with null sign-in activity genuinely hasn't signed in. A user created in 2018 with null activity might be active but pre-dates the telemetry. Check the audit log for recent activity as a secondary signal before classifying as inactive.

Next

IAM1.3 — Data Quality as a Governance Foundation. You've mapped the attributes and measured coverage. IAM1.3 addresses the "you can't govern what you can't see" problem — what happens when lifecycle workflows, dynamic groups, and access reviews run against incomplete data. Remediation strategies: HR system integration, CSV bulk import, Graph API enrichment scripts, and the minimum viable data quality threshold for launching governance. NE data quality assessment and remediation plan.

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