Manual Test Runbook — H3: PIM Eligible Role Templates
Owner: Sagar | Time: ~5 min (Parts A + B, offline) · +30 min Part C (eligibility apply) · +30 min Part D (Graph PATCH for activation rules + live activation test) | Sandbox: snowops-sandbox-tenant-01
Promotes H3 (
modules/azure/pim-templates/) from 🟦 Code Complete → 🟩 Shipped. Part C provisions eligibility (no charge). Part D applies the SnowOps tier-0 + tier-1 activation rules via Graph PATCH and runs a live activation drill. Requires Microsoft Entra ID P2 (PIM is a P2 feature).
Prerequisites
- Sandbox AAD tenant access (Privileged Role Administrator role active)
- Microsoft Entra ID P2 license active on the sandbox tenant
- A dedicated PIM-eligible group per tier:
snowops-tier0-eligible-admins(members can activate Global Admin / etc.)snowops-tier1-eligible-admins(members can activate User Admin / etc.)- At least one PERMANENT Global Admin user who is NOT in the eligible groups (break-glass). Verify before applying. SnowOps standard is 2.
- A second AAD group
snowops-pim-approverswhose members will approve tier-0 activation requests (Step 12 wires this into the approval rule). - 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 (offline, ~2 min)
- Confirm formatting + structural validity of the module:
terraform -chdir=modules/azure/pim-templates fmt -check
terraform -chdir=modules/azure/pim-templates init -backend=false -input=false
terraform -chdir=modules/azure/pim-templates validate
Expected: Success!.
- Run the H3 offline Terratest case:
Expected: 1 top-level test passes.
Part B — full Terratest suite (offline, ~3 min)
- Run the whole offline suite:
Expected: 18 top-level tests pass.
Part C — eligibility apply (real AAD apply, ~30 min)
- Capture the eligible groups + break-glass users:
export SNOWOPS_SANDBOX_TENANT_ID="<sandbox-tenant-guid>"
export SNOWOPS_TIER0_ELIGIBLE_GROUP_ID=$(az ad group show --group snowops-tier0-eligible-admins --query id -o tsv)
export SNOWOPS_TIER1_ELIGIBLE_GROUP_ID=$(az ad group show --group snowops-tier1-eligible-admins --query id -o tsv)
export SNOWOPS_BREAK_GLASS_USER_IDS=$(az ad user list --filter "startsWith(displayName,'snowops-break-glass')" --query '[].id' -o json)
- Apply against the sandbox tenant:
cd tests/terratest/fixtures/pim-templates
terraform init
terraform apply \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID" \
-var "tier0_eligible_group_object_id=$SNOWOPS_TIER0_ELIGIBLE_GROUP_ID" \
-var "tier1_eligible_group_object_id=$SNOWOPS_TIER1_ELIGIBLE_GROUP_ID" \
-var "break_glass_user_object_ids=$SNOWOPS_BREAK_GLASS_USER_IDS"
-
Spot-check the portal (Microsoft Entra ID → Roles and administrators → each tier-0 role → Eligible assignments): the tier-0-eligible group listed as Eligible on Global Admin / Privileged Role Admin / Privileged Authentication Admin. Same for tier-1 (User Admin / Security Admin / etc.).
-
Confirm
terraform outputreturns: tier0_eligibility_assignment_count = 3(3 roles × 1 group = 3)tier1_eligibility_assignment_count = 4(4 roles × 1 group = 4)- Non-empty
tier0_assignment_ids+tier1_assignment_idsmaps - Non-empty
tier0_activation_rule_bodies+tier1_activation_rule_bodies(these are the JSON PATCH bodies Step 9 consumes)
Part D — Graph PATCH activation rules + live drill (~30 min)
Tier-0 + tier-1 role-management policies aren't covered by the azuread provider. The H3 outputs above supply the SnowOps PATCH bodies; this step applies them per role + then validates with a live PIM activation attempt.
- Pre-fetch the per-role roleManagementPolicy IDs:
for role_id in $(terraform output -json tier0_role_template_ids | jq -r '.[]'); do
pa_id=$(az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicyAssignments?\$filter=scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '${role_id}'" \
--query 'value[0].policyId' -o tsv)
echo "${role_id}=${pa_id}" >> /tmp/h3-tier0-policies.txt
done
for role_id in $(terraform output -json tier1_role_template_ids | jq -r '.[]'); do
pa_id=$(az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicyAssignments?\$filter=scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '${role_id}'" \
--query 'value[0].policyId' -o tsv)
echo "${role_id}=${pa_id}" >> /tmp/h3-tier1-policies.txt
done
- PATCH each tier-0 policy's 3 SnowOps rules:
for line in $(cat /tmp/h3-tier0-policies.txt); do
policy_id="${line##*=}"
for rule_name in enablement approval expiration; do
body=$(terraform output -json tier0_activation_rule_bodies | jq -r ".${rule_name}")
rule_id=$(echo "$body" | jq -r '.id')
echo "$body" | az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicies/${policy_id}/rules/${rule_id}" \
--headers 'Content-Type=application/json' \
--body @-
done
done
-
PATCH each tier-1 policy's 2 SnowOps rules (no approval for tier-1):
for line in $(cat /tmp/h3-tier1-policies.txt); do policy_id="${line##*=}" for rule_name in enablement expiration; do body=$(terraform output -json tier1_activation_rule_bodies | jq -r ".${rule_name}") rule_id=$(echo "$body" | jq -r '.id') echo "$body" | az rest --method PATCH \ --uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicies/${policy_id}/rules/${rule_id}" \ --headers 'Content-Type=application/json' \ --body @- done done -
Wire the tier-0 approval rule to
snowops-pim-approvers(the rule body in Step 9 specifies a single-stage approval but doesn't auto-fill the approver list — Graph PATCH this once per tier-0 role):bash APPROVERS_GROUP_ID=$(az ad group show --group snowops-pim-approvers --query id -o tsv) for line in $(cat /tmp/h3-tier0-policies.txt); do policy_id="${line##*=}" az rest --method PATCH \ --uri "https://graph.microsoft.com/v1.0/policies/roleManagementPolicies/${policy_id}/rules/Approval_EndUser_Assignment" \ --headers 'Content-Type=application/json' \ --body @<(cat <<EOF { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyApprovalRule", "id": "Approval_EndUser_Assignment", "setting": { "isApprovalRequired": true, "isApprovalRequiredForExtension": false, "isRequestorJustificationRequired": true, "approvalMode": "SingleStage", "approvalStages": [ { "approvalStageTimeOutInDays": 1, "isApproverJustificationRequired": true, "escalationTimeInMinutes": 0, "isEscalationEnabled": false, "primaryApprovers": [ { "@odata.type": "#microsoft.graph.groupMembers", "groupId": "${APPROVERS_GROUP_ID}" } ] } ] } } EOF ) done -
Live activation drill: as a member of
snowops-tier0-eligible-admins, sign in to https://entra.microsoft.com → Identity governance → Privileged Identity Management → My roles → Eligible assignments → activateGlobal Administrator. Expected behavior:- MFA challenge issued (Enablement_EndUser_Assignment.MultiFactorAuthentication)
- Activation form requires Justification + Ticket # (Ticketing rule on)
- Form requires approval (Approval_EndUser_Assignment with snowops-pim-approvers)
- Max duration capped at 8 hours (Expiration_EndUser_Assignment.maximumDuration = PT8H)
- Activation pending until an approver approves from the same UI
-
Repeat the activation drill for a tier-1 role (e.g., User Admin) as a member of
snowops-tier1-eligible-admins. Expected:- MFA challenge + Justification (no Ticket # required)
- No approval (tier-1 has no approval rule)
- Max duration capped at 4 hours
Pass criteria
- Part A —
terraform validatepasses for the module - Part B — full offline Terratest suite passes (18 top-level tests)
- Part C Step 6 — 3 tier-0 + 4 tier-1 eligible assignments visible in the portal
- Part C Step 7 — assignment counts + activation rule output bodies correct
- Part D Step 9 — each tier-0 policy PATCH returns 200
- Part D Step 10 — each tier-1 policy PATCH returns 200
- Part D Step 11 — approver group wired into all tier-0 approval rules
- Part D Step 12 — tier-0 activation requires MFA + Justification + Ticket + approval; max duration = 8h
- Part D Step 13 — tier-1 activation requires MFA + Justification; max duration = 4h; no approval
- Permanent break-glass tier-0 holders still visible in the portal (Microsoft Entra ID → Roles → Global Administrator → Assignments → Active)
Teardown
cd tests/terratest/fixtures/pim-templates
terraform destroy \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID" \
-var "tier0_eligible_group_object_id=$SNOWOPS_TIER0_ELIGIBLE_GROUP_ID" \
-var "tier1_eligible_group_object_id=$SNOWOPS_TIER1_ELIGIBLE_GROUP_ID" \
-var "break_glass_user_object_ids=$SNOWOPS_BREAK_GLASS_USER_IDS"
Removes the eligibility schedule requests. The role-management-policy rules
(applied via Step 9/10/11 az rest) are NOT reverted by destroy — reset them
manually to Microsoft defaults if needed (Microsoft Entra ID → Roles →
<role> → Role settings → Reset).
Break-glass safety check before destroy. Verify at least one permanent tier-0 holder exists who is NOT in the eligible groups. If
terraform destroystrips eligibility and no permanent holder remains, the tenant becomes unmanageable.
Sign-off
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: