Phase 2 — Entitle virtual application provisioning
Audience: contributor · Profile:
both· Read this when: you are provisioning the Entitle virtual applications that back those groups.Legacy mechanism. This runbook covers Entitle granting dashboard permissions through Entra groups. The current mechanism is a REST integration that works for any user and takes effect immediately — see
../integrations/beyondtrust/entitle-dashboard-permissions.md.
Operator walk-through for the terraform/entitle_user_jit/ module +
web_dashboard/scripts/bootstrap_entitle_app.py wrapper. Validates
Phase 2 of the Entitle user-JIT design.
After Phase 2 deploys, this confirms:
terraform planagainst a clean Entitle tenant prints the expected shape: 3 workflows, N resources (one per dashboard-*Entra group Phase 1 created), 3 policies, 1 bundle.terraform applyprovisions them; a secondterraform applyis a no-op.- In the Entitle UI, the VM Dashboard virtual application appears
with every dashboard-
*resource listed and routed through its tier-appropriate workflow. - The Phase 1 Entra group ids in
oauth_group_mappingsflow cleanly into eachentitle_resource.directory_group_id.
Takes ~30 minutes including the bootstrap script + terraform apply. Run against a real Entitle tenant. Phase 3's E2E loop builds on top.
Prerequisites
- Phase 1 (
bootstrap_entitle_groups.py) has been run against the target Entra tenant.oauth_group_mappingshas one row per group. Confirm via:powershell docker compose exec db psql -U dashboardadmin -d vmclidashboard -c " SELECT display_name, entra_group_id FROM oauth_group_mappings WHERE display_name LIKE 'dashboard-%' ORDER BY display_name;" - An Entitle tenant + API key with permissions to create integrations, workflows, policies, resources, and bundles.
- An existing Entra → Entitle directory integration. Created once via the Entitle UI; capture its id.
- Two approver identifiers (group names or user emails) — one for the single-approver tier, one for the two-approver tier.
- Terraform ≥ 1.5 installed on the runner.
Step 1 — Generate the groups tfvars file
docker compose exec app python -m web_dashboard.scripts.bootstrap_entitle_app `
--output-tfvars /app/terraform/entitle_user_jit/groups.auto.tfvars.json
Expected:
- Wrote N groups -> /app/terraform/entitle_user_jit/groups.auto.tfvars.json
where N is the dashboard-* row count from the Phase 1 prereq query.
- The file contains one entry per group with display_name,
directory_group_id, description, and tier (one of
auto_approve / single_approver / two_approver).
- Re-running overwrites the file deterministically.
If N is 0, stop — Phase 1 hasn't been run, or the groups table is empty. Re-run Phase 1's bootstrap before continuing.
Step 2 — Inspect the tier assignments
cat terraform/entitle_user_jit/groups.auto.tfvars.json | jq '.groups | to_entries | map({key, tier:.value.tier}) | group_by(.tier) | map({tier:.[0].tier, count:length})'
Expected (default 102 prod-seeded groups: 1 admin + 1 baseline +
98 scope/levels + 2 workstreams):
- auto_approve = 32 (baseline + 31 *-read)
- single_approver = 46 (29 *-write + 15 *-use + 2 workgroup-*)
- two_approver = 24 (admin + 23 *-delete)
Exact counts vary with PERMISSION_SCOPE_LEVELS and the number of
workstreams — not every scope offers all four levels, so the totals are
the sum of its value lengths rather than a multiple of the scope count.
If a row landed in the wrong tier, edit _tier_for_group() in
bootstrap_entitle_app.py and re-run Step 1.
*-use routes to single_approver. It was missing from the suffix
lists until recently, so dashboard-secrets-use fell through to the
"tier inference fell through" warning and took the same tier by
default — a log line worth grepping for if you are on an older build.
Step 3 — terraform init
cd terraform/entitle_user_jit
$env:TF_VAR_entitle_api_key = '<your-entitle-api-key>'
terraform init
Expected: Terraform downloads the entitleio/entitle provider (3.2.2 or a later
3.x) and initialises the working directory.
Step 4 — terraform plan (dry run)
terraform plan `
-var "entitle_integration_id=<entra-integration-id>" `
-var "single_approver_group=<entitle-directory-group-name>" `
-var "two_approver_group=<entitle-directory-group-name>"
The approver values are the names of Entitle directory groups, exactly as Entitle shows them. The plan stops unless exactly one group has each name.
Expected: plan shows:
- 3 to add for entitle_workflow (auto_approve / single_approver /
two_approver).
- N to add for entitle_resource_synced.dashboard_group["…"], where N
matches Step 1's count. These are not new resources: each one adopts the resource
Entitle's Entra sync already made for that group, and sets its workflow.
- 0 to change, 0 to destroy.
A group the sync has not picked up yet fails the plan with the provider's "resource not found". Wait for the next sync, or trigger one from the integration in the Entitle UI.
CI runs terraform validate on this module against the real provider schema (the
terraform job), so a schema error here means the provider moved past 3.x.
Step 5 — terraform apply
terraform apply `
-var "entitle_integration_id=<entra-integration-id>" `
-var "single_approver_group=<approver-identifier>" `
-var "two_approver_group=<two-approver-identifier>"
(or via the wrapper: docker compose exec app python -m web_dashboard.scripts.bootstrap_entitle_app --apply --entitle-integration-id … --single-approver-group … --two-approver-group … — the wrapper threads vars through TF_VAR_* env so the API key never lands in shell history.)
Expected: apply completes cleanly. Outputs include:
- workflow_ids — three-entry map.
- resource_ids — N-entry map (dashboard-* → entitle resource id). Paste it into the
dashboard's Settings → Integrations → Entitle panel (resource ID map) for Phase 4's
"Request access" deep links.
- resource_count — N.
Step 6 — Spot-check in the Entitle UI
Sign into your Entitle tenant's web console. Confirm:
- The N
dashboard-*group resources carry the tagVM Dashboard(fromapplication_name) and atier:<tier>tag, so filtering the catalog byVM Dashboardshows exactly them. - Each one's description is the one the bootstrap script wrote.
- Workflows page lists the three new workflows with the names
vm-dashboard-auto-approve,vm-dashboard-single-approver,vm-dashboard-two-approver. - Opening one of the
*-writeresources confirms itsWorkflowfield points atvm-dashboard-single-approver; a*-readresource points atvm-dashboard-auto-approve; an*-deleteoradminresource points atvm-dashboard-two-approver.
Step 7 — Idempotency
terraform apply -var "entitle_integration_id=<…>" -var "single_approver_group=<…>" -var "two_approver_group=<…>"
Expected: No changes. Your infrastructure matches the configuration.
This is the load-bearing property — operators can re-run the bootstrap
script + apply as part of every deployment without risk. Tier
reassignment is a single edit to _tier_for_group() followed by a
re-run; Terraform diffs each resource's workflow and only
updates the affected entries.
Step 8 — Add a new dashboard-* group end-to-end
To prove the Phase 1 → Phase 2 chain works:
- Add a new permission scope to
web_dashboard/api/auth.py:PERMISSION_SCOPE_LEVELS, declaring only the levels something actually enforces. - Re-run Phase 1:
python -m web_dashboard.scripts.bootstrap_entitle_groups --scope=permissions --yes. - Re-run Phase 2 Step 1 + Step 5 (apply).
Expected: Step 5 reports + N to add, where N is the number of levels
the new scope declared — not always three. The new resources now carry the
VM Dashboard tag and their tier's workflow.
Before you add one, read this.
has_permissiontreats every permission map as a strict per-scope allowlist. So adding a scope and gating a route on it revokes that route from every user who has an explicit permission map — with no error, no log line, and no row the administrator who set those permissions ever saw. A new scope needs a backfill inweb_dashboard/database.pyin the same change, granting exactly what a non-admin with an explicit map could already reach: the full level set for a previously ungated route, and nothing for one that required the Admin flag. See Permissions → Adding a scope.
Step 9 — Where this fits
Phase 2 ships the server-side configuration in Entitle. Phase 3 is
the end-to-end request flow — operator A opens Entitle, picks a
dashboard-aws-read resource, gets auto-approved (because that
resource's workflow is vm-dashboard-auto-approve), and the next
dashboard login sees the new permission. The Phase 3 runbook captures
that loop end-to-end against the Entitle tenant configured here.
Phase 4 (UI affordances) consumes the resource IDs that the
resource_ids output surfaces — the dashboard's 403 page deep-links
to the matching resource so users can request access in one click.
Rollback
terraform destroyfrom theterraform/entitle_user_jitdirectory deletes the three workflows and releases the group resources from Terraform state. The resources themselves stay in Entitle (the Entra sync owns them), still pointing at the deleted workflows, so set their workflow in the UI, or re-apply, before anyone requests them. The Entra groups and Phase 1 are untouched.- For partial rollback (e.g. one bad tier assignment), edit
_tier_for_group()and re-run the bootstrap script + apply. The provider's diffing handles the workflow swap. - If
terraform stategets out of sync with the live Entitle tenant (operator deleted a resource via the UI),terraform refreshfollowed by anotherapplyreconciles. The wrapper's tfvars file stays canonical for the desired-state inputs.