Skip to content

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-approvers whose 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_ID env var set
  • Working directory: repo root

Steps

Part A — terraform fmt + validate (offline, ~2 min)

  1. 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!.

  1. Run the H3 offline Terratest case:
cd tests/terratest
go test -v -timeout 5m -run 'TestPIMTemplatesValidate' ./modules/azure/

Expected: 1 top-level test passes.


Part B — full Terratest suite (offline, ~3 min)

  1. Run the whole offline suite:
cd tests/terratest
go test -v -timeout 10m ./...

Expected: 18 top-level tests pass.


Part C — eligibility apply (real AAD apply, ~30 min)

  1. 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)
  1. 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"
  1. 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.).

  2. Confirm terraform output returns:

  3. tier0_eligibility_assignment_count = 3 (3 roles × 1 group = 3)
  4. tier1_eligibility_assignment_count = 4 (4 roles × 1 group = 4)
  5. Non-empty tier0_assignment_ids + tier1_assignment_ids maps
  6. 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.

  1. 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
  1. 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
  1. 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
    
  2. 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

  3. 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 → activate Global 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
  4. 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 validate passes 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 destroy strips eligibility and no permanent holder remains, the tenant becomes unmanageable.


Sign-off

  • Tester: _  |  Date: _  |  Result: PASS / FAIL / N/A
  • Notes: