Skip to main content
This is not an official U.S. federal government website. The Federal Modernization Workbook is independent technical analysis. It does not represent the position of, and has not been reviewed or endorsed by, any federal agency.

Federal Modernization Workbook

Chapter 02 — Contractor Sources and Enforced End Dates

Run contractors from a source other than HR with an end date that can’t be left open: compute each contractor’s earliest end from the contract, the completion date, suitability, the card and the sponsor, write the records as an extract the HR middleware sends through API-driven inbound provisioning, reconcile the register, the card system and the directory read-only, and build the sponsor review that removes access when no one responds, starting from Learn’s bulkUpload API and access package review settings and adding what they leave to you, with labs against fixtures and a test tenant.

A contractor’s end date in a spreadsheet nobody updates keeps access alive. The end date the code computes, from every fact that can end an engagement, doesn’t.

NoteIn this chapter

The code behind the guide’s Gate 2 and the plan’s decision D-I3 and task I5.2: contractors from a named source, an enforced earliest end, the contractor leaver shipped first, and sponsors recertified with remove-on-no-response. You start with Learn’s API-driven inbound provisioning contract (bulkUpload) and the review settings of an access package assignment policy. Then you use examples/hr-lifecycle for what they leave to you: the earliest end per contractor, an extract that examples/hcm-entra turns into SCIM requests, a read-only reconciliation, and the review settings as code. Lab 1 runs on a laptop with no tenant; Lab 2 uses a test tenant.

TipNarrated tour
Building HR-Driven Identity, Chapter 02 — Contractor Sources and Enforced End Dates: narrated tour · 11 minutes · captions from the player's CC button · Download MP4 · All videos
Jump to a slide (19)

Deck · Narrated deck (about 11 minutes, captions) · Script · Captions. Recorded on November 2, 2026 from the approved chapter.

Goal and audience

Goal. By the end of this chapter you can turn a contractor register into records the HR middleware sends, each carrying its earliest end as the leave date and an inactive status once that date has passed; refuse a record with no end or no sponsor; show every contractor account, card and record that disagree; and produce the sponsor review’s settings for review. For each rule you can say whether it is Learn’s contract, Book 03’s design or the program’s own guidance.

Audience. Developers who build the contractor source’s integration, the owner of the contractor register, and the identity engineers who run the provisioning app. You should read Python, CSV and JSON. You don’t need a tenant until Lab 2.

Why contractors need their own code

Contractors aren’t in Federal HR 2.0’s scope, so they come from an agency-owned register mapped into the same canonical record (Book 03, Chapter 06), as the contractor evaluation recommends. The guide’s Step 2 ships the contractor leaver first: the middleware computes the earliest end and sends the leave date and active false itself (Appendix A, Contractor leavers). This chapter is that computation, and the checks around it. The decision of which source to use is the governance body’s (D-I3); the code works with whichever register is named.

Which cloud, for these reads and writes

The records reach Entra ID through the contractor source’s own API-driven provisioning app, through the same middleware as employees. Learn’s feature availability page lists API-driven inbound provisioning, and provisioning agent registration for a US Government tenant, as available in Azure Government, and access reviews as available; GCC uses the commercial service.1 The Perform bulkUpload page itself carries no national cloud table (verification log, row 6). The middleware’s endpoint check accepts only graph.microsoft.com, graph.microsoft.us and dod-graph.microsoft.us (Book 19, Chapter 01). The reconciliation reads GET /users from the cloud you name; graph_export.py has no default.

Start with Microsoft’s procedure

What Microsoft gives you

Learn’s Perform bulkUpload page is the contract: POST /servicePrincipals/{servicePrincipalId}/synchronization/jobs/{jobId}/bulkUpload with Content-Type: application/scim+json, at most 50 user operations per request and a rate limit of 40 requests per second. The provisioning service matches on the attributes the job maps (externalId in the examples) to create, update, enable or disable the account, resolves manager.value as the manager’s externalId, and disables the user when a record sends active false. A successful call returns 202 Accepted with a Location header that points to the provisioning logs. The least-privileged application permission is SynchronizationData-User.Upload.OwnedBy; SynchronizationData-User.Upload is the higher one and the only delegated one, for a User Administrator.2

For recertification, the reviewSettings of an access package assignment policy carry isEnabled, primaryReviewers, fallbackReviewers, schedule and expirationBehavior, whose values are keepAccess, removeAccess and acceptAccessRecommendation.3 Learn’s procedure for a review of an access package offers monthly, quarterly, bi-annual and annual frequencies, Manager as reviewer with a fallback, and the no-response choices no change, remove access or take recommendations.4

What it leaves to you

Each row is pinned by a test in tests/test_writers_and_contractors.py.

Learn gives What it leaves to you What examples/hr-lifecycle adds Test
active false disables the account When a contractor’s engagement has ended The earliest end: contract end, earlier completion date, suitability withdrawn, card terminated, revoked or expired, sponsor gone past the grace period, sponsor recertification due test_earliest_end_takes_the_first_condition, test_an_expired_card_ends_the_engagement
Any record it’s sent Refusing a contractor with no end or no sponsor Quarantine: CS-01, CS-02 test_no_end_date_or_no_sponsor_is_quarantined
A SCIM bulk request schema Mapping the register into the canonical record Rows in the examples/hcm-entra extract layout: worker type C, the sponsor as manager, organization from the sponsor, the earliest end as the termination date test_extract_matches_the_hcm_entra_layout_and_marks_past_ends_inactive
Provisioning results per record A reconciliation before anything changes CR-01 to CR-05 across the register, the card system and the directory test_reconcile_finds_every_class
Review settings as properties The settings the guide asks for, as a reviewed body Bi-annual, the manager as reviewer, a named fallback, remove access on no response test_review_settings_remove_access_when_no_one_responds
— One record per person A register with a key twice is refused test_duplicate_person_keys_are_refused

The verdict. Don’t write a second SCIM client. The middleware in examples/hcm-entra already validates, batches, paces and records provenance; this chapter feeds it the contractor population with dates it computed.

The code: what examples/hr-lifecycle adds

What exists already isn’t rebuilt. examples/hcm-entra builds and sends the SCIM bulk requests, with its own validation, quarantine and provenance manifest (Book 19, Chapter 01); its mapping already treats worker type C as a contractor with code CTR. The validation kit reports enabled member users with no employeeId (VK-HR-01) and the worker type fill rate (VK-HR-07). This chapter adds python/contractor_source.py, config/contractor-rules.json and the contractor-reconcile task.

The earliest end

extract computes, for each register row, every end condition that applies and takes the first. The conditions and their order are Book 03’s (Appendix A, Contractor leavers); the sponsor’s grace period and the 180-day recertification cycle in config/contractor-rules.json are design guidance the agency sets. The kit’s sample grace period is 30 days, where Book 03’s example is 14; its 180-day cycle matches. A record whose end is on or before the run date goes out INACTIVE, so the provisioning service disables the account; every record carries its end as the termination date, so the leaver workflow (Chapter 03) fires on that day.

Rule Severity What it flags Source
CS-01 Finding No contract end date; quarantined The guide: an enforced earliest end
CS-02 Finding Sponsor not in the HR export; quarantined, no manager The evaluation’s mapping: the sponsor as manager
CS-03 Warn Sponsor separated; access ends after the grace period unless a new sponsor is recorded Appendix A
CS-04 Info An engagement ending within 14 days Design guidance
CR-01 Finding An enabled account past its earliest end Appendix A
CR-02 Finding An enabled contractor account with no register record (orphan) Book 03, Chapter 11
CR-03 Finding An active card for a person the register doesn’t hold Book 03, Chapter 11: contractor credential drift
CR-04 Warn An active register record with no account Design guidance
CR-05 Finding employeeLeaveDateTime that isn’t the earliest end Design guidance: the leaver fires on the wrong day

The reconciliation is read only, joined on the enterprise person key in employeeId, as the guide’s step 2 asks before anything changes. Its one Graph call:

Call Least-privileged permission Note
GET /users filtered to employeeType eq 'Contractor', with employeeLeaveDateTime User.Read.All and User-LifeCycleInfo.Read.All Carried from the validation kit’s VK-HR-05 notes; the filter needs ConsistencyLevel: eventual and $count

The sponsor review

review writes assignment-policy-review.json: the reviewSettings block for the contractor package’s assignment policy, bi-annual (absoluteMonthly, interval 6), the requestor’s manager as primary reviewer, a named fallback reviewer and removeAccess on no response, so a missed recertification ends access as the earliest-end rule says. Learn documents the requestorManager subject set for approval stages; its use as a reviewer in reviewSettings isn’t shown on Learn, and Lab 2 confirms it (verification log, row 10). The code sends nothing. Learn’s cmdlet for updating a policy in v1.0 is Set-MgEntitlementManagementAssignmentPolicy, which takes the whole policy as its body, with EntitlementManagement.ReadWrite.All;5 merge the block into the policy you read back and send that. The output’s own _target note names the same cmdlet.

The tests

This chapter’s tests are the seven Chapter 02 tests in tests/test_writers_and_contractors.py, with the shared tests in tests/test_core.py. All 57 tests in the kit pass, with no package, tenant or network.

ImportantIllustrative code

The commands below run the example against a fictional register, card export and directory. They show what the code does, not that a tenant’s provisioning job maps the same attributes. Review the code and the rules, and run Lab 2 in a test tenant, before contractor records reach production.

Lab 1: fixtures, no tenant

You need Python 3.10 or later. Work in examples/hr-lifecycle.

NoteEvery command in Lab 1

Every command in this lab, in order, from examples/hr-lifecycle. Each step below says what it does and what to look for.

# Step 1: List the reconciliation's read and permissions
python python/graph_export.py --task contractor-reconcile --list

# Step 2: Compute the earliest ends and write the extract
python python/contractor_source.py extract --register tests/fixtures/ch02/contractor-register.csv --cards tests/fixtures/ch02/cards.csv \
    --sponsors tests/fixtures/ch02/hr-sponsors.csv --rules config/contractor-rules.json --as-of 2026-11-01 --out out/ch02/extract

# Step 3: Read the decisions
python -c "import json; [print(d['PersonKey'], d['Action'], d['EarliestEnd'], d['Reason']) for d in json.load(open('out/ch02/extract/decisions.json'))]"

# Step 4: Build the SCIM requests with the middleware
python ../hcm-entra/python/hcm_to_scim.py --extract out/ch02/extract/contractors-extract.csv --as-of 2026-11-01 --out out/ch02/scim

# Step 5: Reconcile the register, the cards and the directory
python python/contractor_source.py reconcile --register tests/fixtures/ch02/contractor-register.csv --cards tests/fixtures/ch02/cards.csv \
    --sponsors tests/fixtures/ch02/hr-sponsors.csv --accounts tests/fixtures/ch02/contractor-users.json --rules config/contractor-rules.json \
    --as-of 2026-11-01 --cloud gcc --out out/ch02/reconcile
python -c "import json; [print(r['Rule'], r['Severity'], r['Object'], '|', r['Issue']) for r in json.load(open('out/ch02/reconcile/findings.json'))]"

# Step 6: Write the sponsor review settings
python python/contractor_source.py review --rules config/contractor-rules.json --out out/ch02/review

# Step 7: Run the tests
python -m pytest tests -q

# After the steps
rm -rf out
  1. List the reconciliation’s read and permissions:

    python python/graph_export.py --task contractor-reconcile --list
    Task contractor-reconcile: User-LifeCycleInfo.Read.All, User.Read.All
      GET /users  [User-LifeCycleInfo.Read.All, User.Read.All]

    Terminal window showing python python/graph_export.py --task contractor-reconcile --list and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)
  2. Compute the earliest ends and write the extract:

    python python/contractor_source.py extract --register tests/fixtures/ch02/contractor-register.csv --cards tests/fixtures/ch02/cards.csv \
        --sponsors tests/fixtures/ch02/hr-sponsors.csv --rules config/contractor-rules.json --as-of 2026-11-01 --out out/ch02/extract
    Contractors: 9 records: 3 active, 4 inactive (past their earliest end), 2 quarantined
    Source: 2 Finding, 1 Warn, 1 Info
    Extract written to out/ch02/extract/contractors-extract.csv; build the SCIM requests with examples/hcm-entra/python/hcm_to_scim.py

    Terminal window showing python python/contractor_source.py extract --register tests/fixtures/ch02/con… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    The exit code is 2, because the run has a Finding or a Warn; see the exit codes in the kit’s README.

  3. Read the decisions:

    python -c "import json; [print(d['PersonKey'], d['Action'], d['EarliestEnd'], d['Reason']) for d in json.load(open('out/ch02/extract/decisions.json'))]"
    900001 active 2027-01-11 contract end
    900002 inactive 2026-10-15 completion date
    900003 inactive 2026-10-20 suitability withdrawn
    900004 inactive 2026-10-30 sponsor gone past the grace period
    900005 inactive 2026-10-25 sponsor recertification due
    900006 active 2026-11-10 contract end
    900007 quarantine  no contract end
    900008 quarantine 2027-03-31 contract end
    900009 active 2027-03-01 sponsor recertification due

    Terminal window showing python -c 'import json; [print(d['PersonKey'], d['Action'], d['EarliestEnd'],… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    900007 has no contract end (CS-01); 900008’s sponsor isn’t in the HR export (CS-02). 900004’s sponsor separated on September 30, so the 30-day grace period ends October 30 (CS-03). 900005’s card expired October 28, but its sponsor recertification was due October 25, which comes first.

  4. Build the SCIM requests with the middleware:

    python ../hcm-entra/python/hcm_to_scim.py --extract out/ch02/extract/contractors-extract.csv --as-of 2026-11-01 --out out/ch02/scim
    {"counts": {"ready": 7}, "batches": 1, "disables": 4, "held": false, "hold_reason": ""}

    Terminal window showing python ../hcm-entra/python/hcm_to_scim.py --extract out/ch02/extract/contract… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Seven records in one batch, four of them disables. out/ch02/scim/bulk-request-001.json sends 900002, 900003, 900004 and 900005 with active false. Nothing is sent: the middleware’s dry run is its default.

  5. Reconcile the register, the cards and the directory:

    python python/contractor_source.py reconcile --register tests/fixtures/ch02/contractor-register.csv --cards tests/fixtures/ch02/cards.csv \
        --sponsors tests/fixtures/ch02/hr-sponsors.csv --accounts tests/fixtures/ch02/contractor-users.json --rules config/contractor-rules.json \
        --as-of 2026-11-01 --cloud gcc --out out/ch02/reconcile
    python -c "import json; [print(r['Rule'], r['Severity'], r['Object'], '|', r['Issue']) for r in json.load(open('out/ch02/reconcile/findings.json'))]"
    Reconciliation: 7 Finding, 1 Warn, 0 Info
    Written to out/ch02/reconcile (manifest.json lists every file's SHA-256)
    CR-01 Finding bailey.ortiz@contoso.example | enabled past its earliest end 2026-10-15 (completion date)
    CR-01 Finding cameron.wu@contoso.example | enabled past its earliest end 2026-10-20 (suitability withdrawn)
    CR-01 Finding devon.hale@contoso.example | enabled past its earliest end 2026-10-30 (sponsor gone past the grace period)
    CR-01 Finding ellis.ward@contoso.example | enabled past its earliest end 2026-10-25 (sponsor recertification due)
    CR-02 Finding temp.vendor@contoso.example | enabled contractor account with no register record (orphan)
    CR-03 Finding 900011 | active card for a person the contractor source doesn't hold (contractor credential drift)
    CR-05 Finding finley.moss@contoso.example | employeeLeaveDateTime 2026-12-10 isn't the earliest end 2026-11-10; the leaver workflow fires on the wrong day
    CR-04 Warn indy.shah@contoso.example | active register record with no account

    Terminal window showing python python/contractor_source.py reconcile --register tests/fixtures/ch02/c… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    The four CR-01 accounts are the four disables in step 4: once the requests are sent, the next reconciliation shows none, which is the guide’s evidence for the gate.

  6. Write the sponsor review settings:

    python python/contractor_source.py review --rules config/contractor-rules.json --out out/ch02/review

    Terminal window showing python python/contractor_source.py review --rules config/contractor-rules.jso… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Review settings written to out/ch02/review/assignment-policy-review.json for review; nothing was sent. The block has expirationBehavior removeAccess, the requestor’s manager at level 1 as primary reviewer, and an absoluteMonthly recurrence with interval 6.

  7. Run the tests:

    python -m pytest tests -q

    Terminal window showing python -m pytest tests -q and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

Expected result: 57 passed. Delete out when you’re done:

rm -rf out

Terminal window showing rm -rf out and what it printed

What the terminal shows, run on the kit’s fixtures (the same output as text)

Lab 2: test tenant

Not run by the authors. Use a test tenant in the worldwide or GCC cloud, never production. Work in examples/hr-lifecycle. You’ll need my-register.csv, my-cards.csv and my-sponsors.csv: your test contractors, their cards and their sponsors, in the layout of the files in tests/fixtures/ch02. You’ll replace: <today> (today’s date, yyyy-mm-dd), <provisioning-api-endpoint> (the Provisioning API Endpoint on the contractor provisioning app’s overview page), <scim-token> (a bearer token for graph.microsoft.com issued to the middleware’s identity), <graph-token> (a bearer token for graph.microsoft.com issued to the reader app), <tenant-id> (the test tenant’s ID) and <policy-id> (the ID of the test contractor package’s assignment policy).

NoteEvery command in Lab 2

Every command in this lab, in order, from examples/hr-lifecycle. Each step below says what it does and what to look for.

In a POSIX shell (Git Bash on Windows):

# Step 2: Send two test contractors
python python/contractor_source.py extract --register my-register.csv --cards my-cards.csv --sponsors my-sponsors.csv --rules config/contractor-rules.json --as-of <today> --out out/ch02-test-extract
cp ../hcm-entra/config/settings.example.json ../hcm-entra/config/settings.json
python -c "import json; p='../hcm-entra/config/settings.json'; d=json.load(open(p)); d['endpoint'] = '<provisioning-api-endpoint>'; json.dump(d, open(p, 'w'), indent=2)"
export HCM_ENTRA_BEARER_TOKEN='<scim-token>'
python ../hcm-entra/python/hcm_to_scim.py --extract out/ch02-test-extract/contractors-extract.csv --settings ../hcm-entra/config/settings.json --as-of <today> --out out/ch02-test-scim --apply
python -c "import json; [print(r['batch'], r['status'], r['provisioning_logs']) for r in json.load(open('out/ch02-test-scim/apply-results.json'))]"

# Step 3: Reconcile against the tenant
export HRLC_GRAPH_TOKEN='<graph-token>'
python python/graph_export.py --task contractor-reconcile --cloud gcc --out exports/ch02
python python/contractor_source.py reconcile --register my-register.csv --cards my-cards.csv --sponsors my-sponsors.csv \
    --accounts exports/ch02/contractor-users.json --rules config/contractor-rules.json --as-of <today> --cloud gcc --out out/ch02-test

In PowerShell 7:

# Step 4: Apply the review settings to a test contractor package
python python/contractor_source.py review --rules config/contractor-rules.json --out out/ch02-test-review
Connect-MgGraph -Environment Global -TenantId <tenant-id> -Scopes EntitlementManagement.ReadWrite.All
$uri = 'https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentPolicies/<policy-id>'
$before = Invoke-MgGraphRequest -Method GET -Uri $uri
$before.Remove('@odata.context')
$before | ConvertTo-Json -Depth 20 | Set-Content policy-before.json
$body = Get-Content policy-before.json -Raw | ConvertFrom-Json -AsHashtable
$body['reviewSettings'] = (Get-Content out/ch02-test-review/assignment-policy-review.json -Raw | ConvertFrom-Json -AsHashtable)['reviewSettings']
$body | ConvertTo-Json -Depth 20 | Set-Content policy-new.json

# Step 4: Apply the review settings to a test contractor package
Set-MgEntitlementManagementAssignmentPolicy -AccessPackageAssignmentPolicyId <policy-id> -BodyParameter (Get-Content policy-new.json -Raw | ConvertFrom-Json -AsHashtable)
(Invoke-MgGraphRequest -Method GET -Uri $uri).reviewSettings.primaryReviewers | ConvertTo-Json -Depth 5

# Step 4: Apply the review settings to a test contractor package
Invoke-MgGraphRequest -Method GET -Uri $uri | ConvertTo-Json -Depth 20 | Set-Content policy-portal.json

# Step 5: Back it out
Set-MgEntitlementManagementAssignmentPolicy -AccessPackageAssignmentPolicyId <policy-id> -BodyParameter (Get-Content policy-before.json -Raw | ConvertFrom-Json -AsHashtable)

In a POSIX shell (Git Bash on Windows):

# Step 5: Back it out
rm -rf exports/ch02 out/ch02-test out/ch02-test-extract out/ch02-test-scim out/ch02-test-review policy-before.json policy-new.json policy-portal.json
  1. Set up the contractor provisioning app as Learn’s API-driven inbound provisioning procedure describes, with the attribute mappings examples/hcm-entra documents, and grant the middleware’s identity SynchronizationData-User.Upload.OwnedBy on that app only.6

  2. Send two test contractors through the middleware’s apply path, as Book 19, Chapter 01’s Lab 2 describes: one active, one past its end. The commands write the extract from your files, put the endpoint in the middleware’s settings, send the batch, and print each batch’s status and the Location header it returned:

    python python/contractor_source.py extract --register my-register.csv --cards my-cards.csv --sponsors my-sponsors.csv --rules config/contractor-rules.json --as-of <today> --out out/ch02-test-extract
    cp ../hcm-entra/config/settings.example.json ../hcm-entra/config/settings.json
    python -c "import json; p='../hcm-entra/config/settings.json'; d=json.load(open(p)); d['endpoint'] = '<provisioning-api-endpoint>'; json.dump(d, open(p, 'w'), indent=2)"
    export HCM_ENTRA_BEARER_TOKEN='<scim-token>'
    python ../hcm-entra/python/hcm_to_scim.py --extract out/ch02-test-extract/contractors-extract.csv --settings ../hcm-entra/config/settings.json --as-of <today> --out out/ch02-test-scim --apply
    python -c "import json; [print(r['batch'], r['status'], r['provisioning_logs']) for r in json.load(open('out/ch02-test-scim/apply-results.json'))]"

    Read the provisioning log entry each Location header points to, and confirm the second account is disabled and both carry the leave date.

  3. Reconcile against the tenant, with a token for graph.microsoft.com in HRLC_GRAPH_TOKEN for an app holding User.Read.All and User-LifeCycleInfo.Read.All:

    export HRLC_GRAPH_TOKEN='<graph-token>'
    python python/graph_export.py --task contractor-reconcile --cloud gcc --out exports/ch02
    python python/contractor_source.py reconcile --register my-register.csv --cards my-cards.csv --sponsors my-sponsors.csv \
        --accounts exports/ch02/contractor-users.json --rules config/contractor-rules.json --as-of <today> --cloud gcc --out out/ch02-test

    There should be no CR-01 for the account step 2 disabled.

  4. Apply the review settings to a test contractor package. In PowerShell 7 with Graph PowerShell, the commands write the review settings, read the policy back into policy-before.json (your read-back), and write policy-new.json: the read-back with reviewSettings from assignment-policy-review.json merged in:

    python python/contractor_source.py review --rules config/contractor-rules.json --out out/ch02-test-review
    Connect-MgGraph -Environment Global -TenantId <tenant-id> -Scopes EntitlementManagement.ReadWrite.All
    $uri = 'https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentPolicies/<policy-id>'
    $before = Invoke-MgGraphRequest -Method GET -Uri $uri
    $before.Remove('@odata.context')
    $before | ConvertTo-Json -Depth 20 | Set-Content policy-before.json
    $body = Get-Content policy-before.json -Raw | ConvertFrom-Json -AsHashtable
    $body['reviewSettings'] = (Get-Content out/ch02-test-review/assignment-policy-review.json -Raw | ConvertFrom-Json -AsHashtable)['reviewSettings']
    $body | ConvertTo-Json -Depth 20 | Set-Content policy-new.json

    Review policy-new.json, then send it with Set-MgEntitlementManagementAssignmentPolicy,7 and read the primary reviewers back:

    Set-MgEntitlementManagementAssignmentPolicy -AccessPackageAssignmentPolicyId <policy-id> -BodyParameter (Get-Content policy-new.json -Raw | ConvertFrom-Json -AsHashtable)
    (Invoke-MgGraphRequest -Method GET -Uri $uri).reviewSettings.primaryReviewers | ConvertTo-Json -Depth 5

    Confirm the reviewer is the manager. If the service refuses requestorManager as a reviewer, set Manager as reviewer in the admin center as Learn’s procedure shows,8 export the policy, and record the subject set it uses:

    Invoke-MgGraphRequest -Method GET -Uri $uri | ConvertTo-Json -Depth 20 | Set-Content policy-portal.json
  5. Back it out. Restore the policy’s previous reviewSettings from your read-back:

    Set-MgEntitlementManagementAssignmentPolicy -AccessPackageAssignmentPolicyId <policy-id> -BodyParameter (Get-Content policy-before.json -Raw | ConvertFrom-Json -AsHashtable)

    Pause the provisioning job (a paused job keeps its state), delete the test contractors, and remove the permissions. Then delete exports/ch02 and out/ch02-test, with the lab’s other output folders and policy files:

    rm -rf exports/ch02 out/ch02-test out/ch02-test-extract out/ch02-test-scim out/ch02-test-review policy-before.json policy-new.json policy-portal.json

Verified facts

Item Value US Government clouds Stage Source
Perform bulkUpload POST /servicePrincipals/{id}/synchronization/jobs/{jobId}/bulkUpload; application/scim+json; 50 operations per request; 40 requests per second; 202 Accepted with a Location header to the provisioning logs No national cloud table on the page; API-driven inbound provisioning listed as available in Azure Government v1.0 9 10
bulkUpload permissions Application: SynchronizationData-User.Upload.OwnedBy (least), SynchronizationData-User.Upload; delegated: SynchronizationData-User.Upload, for a User Administrator — v1.0 11
Matching and state Matching attributes per the job’s mapping; active false disables; manager.value is the manager’s externalId — v1.0 12
Review settings isEnabled, primaryReviewers, fallbackReviewers, schedule, expirationBehavior (keepAccess, removeAccess, acceptAccessRecommendation) Access reviews available in Azure Government v1.0 13 14
Access review of a package (procedure) Monthly, quarterly, bi-annual or annual; Manager as reviewer with a fallback; no response: no change, remove access or take recommendations — GA 15
requestorManager A subject set for a requestor’s manager, managerLevel 1 or 2, documented for approval stages — v1.0 16
Update an assignment policy Set-MgEntitlementManagementAssignmentPolicy, EntitlementManagement.ReadWrite.All — v1.0 17
List users with leave date User.Read.All with User-LifeCycleInfo.Read.All — v1.0 Carried from the validation kit’s VK-HR-05

Places Learn’s pages differ or say nothing:

  • requestorManager as a reviewer. Learn documents it for approvals, and the portal offers Manager as a reviewer, but no v1.0 page shows the subject set in reviewSettings. Lab 2 confirms it.
  • bulkUpload in the national clouds. The API page has no table; the feature availability page lists API-driven inbound provisioning in Azure Government. The program reads that as the API being available there.

Federal requirements and evidence

This chapter helps prove the guide’s Gate 2; its requirements table is the source, and the federal framework crosswalk lists every federal source.

Requirement KSI What this code gives What the ISSO keeps
NIST SP 800-53 AC-2 and PS-7, Moderate KSI-IAM-JIT (AC-2); KSI-SCR-MON (PS-7) The decisions with each earliest end and its reason; the read-only reconciliation, run before the contractor leaver ships and again after, with zero CR-01 Both reconciliation output folders with their manifests; end-date-to-block intervals from Chapter 06
IA-4(4) KSI-IAM-AAM, KSI-IAM-ELP, KSI-IAM-JIT Worker type C on every record, mapped to CTR by the middleware The extract and the middleware’s provenance manifest
OMB M-22-09 and M-19-17 None: not SP 800-53 controls The register as one named source per fact The authority-per-fact table, signed by the governance body (not produced by code)
Privacy Act basis for each contractor record flow None — The privacy office’s determination for the register and the card system
SA-9, external system services: FedRAMP use of the HR platform’s cloud service, which runs on Oracle Cloud Infrastructure KSI-SCR-MIT, KSI-SCR-MON (reported by the provider) — The FedRAMP authorization the agency relies on for the HR platform, as the guide’s Gate 2 table says

Each scripted step in the labs, and each file the code writes, with the evidence it leaves:

Lab step What it runs or writes Requirement KSI Evidence
Lab 1, step 1 graph_export.py --task contractor-reconcile --list AC-6, least privilege: GET calls only KSI-IAM-ELP The printed list, and config/calls.json as reviewed
Lab 1, step 2 contractor_source.py extract, writing contractors-extract.csv PS-7; IA-4(4) for worker type C KSI-SCR-MON; KSI-IAM-AAM out/ch02/extract/contractors-extract.csv, hashed in the folder’s manifest.json
Lab 1, steps 2 and 3 decisions.json AC-2 and PS-7: each earliest end and its reason KSI-IAM-JIT; KSI-SCR-MON Each PersonKey with its Action, EarliestEnd and Reason, as step 3 prints them
Lab 1, step 2 findings.json (CS-01 to CS-04) AC-2 KSI-IAM-JIT The quarantined records and their owners
Lab 1, steps 2, 5 and 6 manifest.json in each output folder AU-9; SI-7 KSI-MLA-OSM; KSI-SVC-VRI A SHA-256 per file, written once
Lab 1, step 4 hcm_to_scim.py, writing bulk-request-001.json, validation-report.json, quarantine.json and provenance.json IA-4(4): C mapped to CTR KSI-IAM-AAM, KSI-IAM-ELP, KSI-IAM-JIT out/ch02/scim/provenance.json and validation-report.json
Lab 1, step 5 contractor_source.py reconcile, writing decisions.json and findings.json (CR-01 to CR-05) AC-2 account review; PS-7 KSI-IAM-JIT; KSI-SCR-MON out/ch02/reconcile/findings.json, with zero CR-01 after the leaver ships
Lab 1, step 6 contractor_source.py review, writing assignment-policy-review.json AC-2 account review: the sponsor recertifies, and no response removes access KSI-IAM-JIT out/ch02/review/assignment-policy-review.json, matched against the policy in the tenant
Lab 1, step 7 python -m pytest tests -q CM-3(2) (design guidance) KSI-CMT-VTD 57 passed, kept with the change
Lab 1, clean-up rm -rf out None: fixture output isn’t evidence — —
Lab 2, step 2 extract from your files; set the endpoint; hcm_to_scim.py --apply, writing apply-results.json AC-2(1), automated account management; PS-7 KSI-IAM-JIT; KSI-SCR-MON out/ch02-test-scim/apply-results.json: each batch’s status and provisioning log link
Lab 2, step 3 graph_export.py --task contractor-reconcile, writing contractor-users.json and export.json; reconcile into out/ch02-test AC-2 account review; PS-7; AC-6 for GET only KSI-IAM-JIT; KSI-SCR-MON; KSI-IAM-ELP out/ch02-test/findings.json and its manifest.json; exports/ch02/export.json
Lab 2, step 4 review; read the policy into policy-before.json; write policy-new.json; Set-MgEntitlementManagementAssignmentPolicy; read back into policy-portal.json AC-2 account review; CM-3 for the change KSI-IAM-JIT; KSI-CMT-LMC policy-before.json, policy-new.json, the reviewers read back, and the directory audit record of the update
Lab 2, step 5 Restore policy-before.json; rm -rf the lab files CM-3: the change backed out KSI-CMT-LMC The directory audit record of the restore

KSIs are FedRAMP 20x indicators for cloud service providers: the agency authorizes its own system and lines this evidence up with what its providers report against them, and it doesn’t report KSIs itself (Book 00, Appendix B). Each KSI shown lists the row’s control among its related SP 800-53 controls on FedRAMP’s page;18 which file supports which indicator is the program’s mapping (design guidance). Controls the guide’s table doesn’t name (AC-2(1), AC-6, AU-9, CM-3, CM-3(2), SI-7) are in the Moderate baseline (Book 13, Appendix A; Book 14, Appendix A).

Dangers

Danger What happens The guard
A trusted spreadsheet end date Access outlives the engagement The earliest end from every condition; CR-01
An open-ended contractor No leaver ever fires CS-01 quarantines the record
A sponsor who left Nobody is accountable for the contractor CS-03, and the end after the grace period
A missed recertification Access stays because no one answered removeAccess on no response, and the recertification date as an end condition
A leave date in the directory that drifted The leaver workflow fires on the wrong day CR-05
A card for someone the source doesn’t hold A credential works for a person nobody sponsors CR-03, and Chapter 07’s contractor credential drift
A second SCIM client Two writers, two sets of validation The extract goes through examples/hcm-entra; the register names one writer (Chapter 01)

Checklist

Back to top

Footnotes

  1. Microsoft Learn, “Microsoft Entra feature availability,” https://learn.microsoft.com/entra/identity/authentication/feature-availability (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  2. Microsoft Learn, “Perform bulkUpload,” permissions, HTTP request, response and examples, https://learn.microsoft.com/graph/api/synchronization-synchronizationjob-post-bulkupload?view=graph-rest-1.0 (read November 1, 2026).↩︎

  3. Microsoft Learn, “accessPackageAssignmentReviewSettings resource type,” https://learn.microsoft.com/graph/api/resources/accesspackageassignmentreviewsettings?view=graph-rest-1.0 (read November 1, 2026).↩︎

  4. Microsoft Learn, “Create an access review of an access package in entitlement management,” https://learn.microsoft.com/entra/id-governance/entitlement-management-access-reviews-create (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  5. Microsoft Learn, “Set-MgEntitlementManagementAssignmentPolicy,” permissions and example, https://learn.microsoft.com/powershell/module/microsoft.graph.identity.governance/set-mgentitlementmanagementassignmentpolicy?view=graph-powershell-1.0 (read November 1, 2026).↩︎

  6. Microsoft Learn, “Perform bulkUpload,” permissions, HTTP request, response and examples, https://learn.microsoft.com/graph/api/synchronization-synchronizationjob-post-bulkupload?view=graph-rest-1.0 (read November 1, 2026).↩︎

  7. Microsoft Learn, “Set-MgEntitlementManagementAssignmentPolicy,” permissions and example, https://learn.microsoft.com/powershell/module/microsoft.graph.identity.governance/set-mgentitlementmanagementassignmentpolicy?view=graph-powershell-1.0 (read November 1, 2026).↩︎

  8. Microsoft Learn, “Create an access review of an access package in entitlement management,” https://learn.microsoft.com/entra/id-governance/entitlement-management-access-reviews-create (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  9. Microsoft Learn, “Perform bulkUpload,” permissions, HTTP request, response and examples, https://learn.microsoft.com/graph/api/synchronization-synchronizationjob-post-bulkupload?view=graph-rest-1.0 (read November 1, 2026).↩︎

  10. Microsoft Learn, “Microsoft Entra feature availability,” https://learn.microsoft.com/entra/identity/authentication/feature-availability (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  11. Microsoft Learn, “Perform bulkUpload,” permissions, HTTP request, response and examples, https://learn.microsoft.com/graph/api/synchronization-synchronizationjob-post-bulkupload?view=graph-rest-1.0 (read November 1, 2026).↩︎

  12. Microsoft Learn, “Perform bulkUpload,” permissions, HTTP request, response and examples, https://learn.microsoft.com/graph/api/synchronization-synchronizationjob-post-bulkupload?view=graph-rest-1.0 (read November 1, 2026).↩︎

  13. Microsoft Learn, “accessPackageAssignmentReviewSettings resource type,” https://learn.microsoft.com/graph/api/resources/accesspackageassignmentreviewsettings?view=graph-rest-1.0 (read November 1, 2026).↩︎

  14. Microsoft Learn, “Microsoft Entra feature availability,” https://learn.microsoft.com/entra/identity/authentication/feature-availability (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  15. Microsoft Learn, “Create an access review of an access package in entitlement management,” https://learn.microsoft.com/entra/id-governance/entitlement-management-access-reviews-create (read November 1, 2026; carried from the HR-driven identity implementation guide’s verification log).↩︎

  16. Microsoft Learn, “requestorManager resource type,” https://learn.microsoft.com/graph/api/resources/requestormanager?view=graph-rest-1.0 (read November 1, 2026).↩︎

  17. Microsoft Learn, “Set-MgEntitlementManagementAssignmentPolicy,” permissions and example, https://learn.microsoft.com/powershell/module/microsoft.graph.identity.governance/set-mgentitlementmanagementassignmentpolicy?view=graph-powershell-1.0 (read November 1, 2026).↩︎

  18. FedRAMP, “Key Security Indicators,” FedRAMP Consolidated Rules for 2026, indicator titles and “Related SP 800-53 Controls,” including the mapping updates of October 13, 2026, https://www.fedramp.gov/2026/reference/key-security-indicators/ (read November 2, 2026; the indicator IDs are carried from Book 13, Appendix A and Book 14, Chapter 03).↩︎