Manual Test Runbook — H4: SCIM Provisioning to SaaS¶
Owner: Sagar | Time: ~5 min (Parts A + B, offline) · +45 min Part C (live SCIM provisioning against a real SaaS endpoint) | Sandbox: snowops-sandbox-tenant-01
Promotes H4 (
modules/azure/scim-provisioning/) from 🟦 Code Complete → 🟩 Shipped. Part C provisions a real enterprise app, points it at a live SaaS SCIM endpoint, starts the synchronization job, and confirms a test user provisions end-to-end. Completes the identity-lifecycle story alongside H1 (Entra baseline), H2 (Conditional Access), and H3 (PIM).
Prerequisites¶
- Sandbox AAD tenant access (Application Administrator role active)
- A target SaaS app that supports SCIM v2 provisioning, with:
- its gallery template ID (from the Entra app gallery), or the generic
non-gallery SCIM template
8adf8e6e-67b2-4cf2-a259-e3dc5476c621 - its SCIM v2 endpoint URL (the tenant URL, https)
- a bearer token issued by the SaaS app for SCIM
- A test AAD user (e.g.
snowops-scim-test@…) in scope for provisioning - Local tooling:
terraform >= 1.6,go >= 1.22,az CLI >= 2.50,jq -
SNOWOPS_SANDBOX_TENANT_IDenv var set - Working directory: repo root
Steps¶
Part A — terraform fmt + validate + go vet (offline, ~2 min)¶
- Confirm formatting + structural validity of the module:
terraform -chdir=modules/azure/scim-provisioning fmt -check -recursive
terraform -chdir=modules/azure/scim-provisioning init -backend=false -input=false
terraform -chdir=modules/azure/scim-provisioning validate
Sandbox note.
init/validatereachregistry.terraform.ioto fetch the azuread provider. In a network-restricted sandbox that returns403 Forbiddenand init fails — this is environmental, not a module defect (every sibling azuread fixture, e.g. conditional-access, fails init identically). Runvalidatefrom a host with registry access (CI runner) to seeSuccess!.
- Run the H4 offline Terratest case +
go vet:
cd tests/terratest
go vet ./modules/azure/
go test -v -timeout 5m -run 'TestScimProvisioningValidate' ./modules/azure/
Expected: go vet clean; 1 top-level test passes (CI / registry-reachable
host).
Part B — what SCIM provisioning is + the full offline suite (~3 min)¶
What H4 does. Microsoft Entra SCIM provisioning pushes the user lifecycle from Entra to a downstream SaaS app automatically. A synchronization job on the enterprise app's service principal runs on a schedule (~40 min cycle), reads in-scope users/groups from Entra, and calls the SaaS app's SCIM v2 endpoint to create joiners, update movers (attribute changes), and deactivate leavers. This replaces manual account management in each SaaS console.
The three prerequisites (no SCIM, no provisioning): (1) a gallery template ID for the app —
azuread_application_from_templateinstantiates the enterprise app from it and wires in the gallery synchronization templates; (2) the SaaS app's SCIM v2 endpoint URL — stored as theBaseAddresssynchronization secret; (3) a bearer token from the SaaS app — stored as theSecretTokensecret. The module sets both viaazuread_synchronization_secret, then provisionsazuread_synchronization_job.Attribute mappings note. The gallery template ships default Entra→SCIM attribute mappings (userPrincipalName→userName, mail, displayName, …) and a default scope. The azuread provider exposes no resource for mappings or scoping, so any per-client customization (which attributes flow, which groups are in scope) is done in the portal or via Graph — see Part C Step 6.
- Run the whole offline suite (registry-reachable host):
Expected: all top-level tests pass, including TestScimProvisioningValidate.
Part C — live SCIM provisioning against a real SaaS endpoint (~45 min)¶
- Gather the SaaS app inputs and apply against the sandbox tenant:
export SNOWOPS_SANDBOX_TENANT_ID="<sandbox-tenant-guid>"
export SNOWOPS_SCIM_TEMPLATE_ID="<gallery-template-guid>" # or 8adf8e6e-... for non-gallery
export SNOWOPS_SCIM_BASE_ADDRESS="https://api.<saas>.example.com/scim/v2"
export SNOWOPS_SCIM_TOKEN="<bearer-token-from-saas>"
cd tests/terratest/fixtures/scim-provisioning
terraform init
terraform apply \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID" \
-var "gallery_template_id=$SNOWOPS_SCIM_TEMPLATE_ID" \
-var "scim_base_address=$SNOWOPS_SCIM_BASE_ADDRESS" \
-var "scim_secret_token=$SNOWOPS_SCIM_TOKEN"
-
Confirm
terraform outputreturns a non-emptyapplication_id,service_principal_object_id,synchronization_job_id, and the echoedscim_base_address(the token is deliberately NOT an output). -
In the portal (Microsoft Entra ID → Enterprise applications →
SnowOps - Validate SaaS Provisioning→ Provisioning): - Confirm Provisioning Mode = Automatic and the Admin Credentials (Tenant URL = your SCIM base address, Secret Token set).
- Click Test Connection — Entra performs a SCIM handshake against the endpoint with the bearer token. Expected: success.
- Review Mappings (the gallery defaults) and Scope — adjust attribute mappings / assign the test user or a scoping group as the client requires. (These are not managed by Terraform.)
-
Configure the quarantine Notification Email (the module records
notification_emailfor evidence; set it here in the portal). -
Provision a test user on demand and confirm it lands in the SaaS app:
SP_ID=$(terraform output -raw service_principal_object_id)
JOB_ID=$(terraform output -raw synchronization_job_id)
# Confirm the job is running (enabled on apply).
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/${SP_ID}/synchronization/jobs/${JOB_ID}" \
--query 'status.code' -o tsv # Expected: Active (or Quarantine if the endpoint is failing)
# Trigger an on-demand provisioning of the test user.
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/${SP_ID}/synchronization/jobs/${JOB_ID}/provisionOnDemand" \
--headers 'Content-Type=application/json' \
--body '{"parameters":[{"subjects":[{"objectId":"<test-user-object-id>","objectIdProperty":"ObjectId"}],"ruleId":"<provisioning-rule-id>"}]}'
Then log into the SaaS app's admin console and confirm the test user now has an account (joiner). Optionally update an attribute in Entra and confirm it propagates (mover); disable/remove the user and confirm it's deactivated in the SaaS app on the next cycle (leaver).
- Check the Provisioning logs (Enterprise application → Provisioning →
View provisioning logs) for the test user — expect a
Create/Updateentry with status Success against the SCIM endpoint.
Pass criteria¶
- Part A —
terraform fmt -check -recursiveclean;go vetclean;validatepasses on a registry-reachable host (sandbox init 403 is environmental) - Part B — full offline Terratest suite passes incl.
TestScimProvisioningValidate - Part C Step 5 — apply succeeds; outputs populated; token NOT in outputs
- Part C Step 6 — portal Test Connection succeeds against the SCIM endpoint
- Part C Step 7 — synchronization job status
Active; test user provisioned (joiner visible in the SaaS app) - Part C Step 8 — provisioning log shows a Success entry for the test user
Teardown¶
cd tests/terratest/fixtures/scim-provisioning
terraform destroy \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID" \
-var "gallery_template_id=$SNOWOPS_SCIM_TEMPLATE_ID" \
-var "scim_base_address=$SNOWOPS_SCIM_BASE_ADDRESS" \
-var "scim_secret_token=$SNOWOPS_SCIM_TOKEN"
Stops + removes the synchronization job, the SCIM secret, the service principal, and the application created from the template.
Deprovisioning caveat before destroy. Destroying H4 stops the job — Entra stops pushing leaver events. Accounts already created in the SaaS app are NOT removed by
terraform destroy; clean up the test user in the SaaS console after teardown so the sandbox doesn't accumulate orphan accounts.
Sign-off¶
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: