-
Notifications
You must be signed in to change notification settings - Fork 1
How It Works
The PIMActivation Portal is a single-page application. It has no backend, no proxy, and no server-side session. Everything runs in your browser — including authentication, policy resolution, the batch engine, and progressive rendering. This page walks through the architecture from boot to bulk activation.
Portal/
├── index.html
├── css/portal.css
├── js/
│ ├── app.js # Bootstrap and event wiring
│ ├── auth.js # MSAL wrapper (redirect flow, claims threading)
│ ├── msal-config.js # Client / tenant ID — injected at deploy time
│ ├── policy-cache.js # 30-minute in-memory policy cache
│ ├── profiles.js # IndexedDB profile manager
│ ├── roles.js # Progressive rendering and expiry timers
│ └── api/
│ ├── arm-client.js # ARM activate / deactivate
│ ├── batch-client.js # Bulk engine (Graph $batch + ARM concurrency)
│ └── graph-client.js # Graph + $batch with 429 retry
├── manifest.json
└── staticwebapp.config.json # SPA routing, CSP, security headers
No bundler, no framework — vanilla JavaScript loaded as <script> tags from index.html in dependency order.
-
index.htmlloads MSAL.js v5 fromcdn.jsdelivr.net(the only allowed third-party origin under the CSP), then loadsmsal-config.js, the API clients, and finallyapp.js. -
app.jsconstructs an MSALPublicClientApplicationfromwindow.msalConfig, processes any pending redirect response, and either renders the signed-in shell or shows the sign-in CTA. - On sign-in, the portal acquires tokens for Graph and ARM scopes (defined in
msal-config.js) silently and starts fetching roles in parallel.
auth.js wraps MSAL with three responsibilities:
-
Sign-in / sign-out via
loginRedirect/logoutRedirect. The redirect flow is used (not popup) so token acquisition survives strict popup blockers and works on mobile. -
Token acquisition via
acquireTokenSilentwith fallback toacquireTokenRedirectoninteraction_required. -
Claims threading — when an API call fails with a Conditional Access claims challenge, the portal stores the pending operation, calls
acquireTokenRedirectwith the decodedclaimsparameter, and then threads the sameclaimsinto every subsequent token acquisition for the operation.
Tokens are cached in sessionStorage only. There is no refresh-token persistence.
- Issues GET requests for eligibility / assignment schedules and policies.
- Bundles bulk POSTs into Microsoft Graph
$batchrequests, chunked at 20 (the Graph batch limit). - Detects
429responses, honorsRetry-Afterheaders, and applies exponential backoff. - Detects
401withWWW-Authenticate: insufficient_claims(or claims-bearing JSON body) and surfaces aClaimsChallengeErrorto the orchestrator.
- Discovers Azure Resource role eligibilities and assignments via tenant-root
asTarget()queries — no per-subscription enumeration. - PUTs activation requests against
roleAssignmentScheduleRequests. - Surfaces the same claims-challenge errors so the orchestrator can step up.
The orchestrator that powers bulk activation:
- Splits the selected roles by plane.
- Routes Entra and Group operations through Graph
$batch(20 per request). - Runs Azure Resource operations as concurrent ARM calls under a small concurrency limit, using
Promise.allSettledso one failure does not abort the batch. - Aggregates per-role outcomes into the activity history.
- On a claims challenge from any plane, pauses the batch, performs the step-up, then resumes with the new claims threaded in.
policy-cache.js fetches role policies in bulk:
- For Entra, policies are fetched via tenant-root role-management policy assignment queries to avoid per-Administrative-Unit permission issues.
- For Groups and Azure Resources, per-policy fetches are made on first use.
- Results are cached in memory for 30 minutes keyed by tenant.
The policy object exposes the matrix the UI renders: requiresJustification, requiresTicket, requiresMfa, requiresAuthContext (with authContextId), requiresApproval, and maxDurationHours.
roles.js is the rendering brain:
- Holds an in-memory map of roles keyed by a stable
uidso selection state survives refreshes. - Renders eligible and active tables progressively as plane fetches resolve — Entra and Group results appear first; Azure usually arrives shortly after.
- Runs a 30-second tick to update the live countdown on active roles and recompute colour-coded urgency.
- De-duplicates Azure roles when the same role is inherited from multiple ancestor scopes (the closest ancestor wins).
profiles.js manages a single IndexedDB object store. Each profile carries an id, name, an array of role uids, optional pre-filled justification / ticket / duration override, an optional tenant id (when tenant-scoped profiles are enabled), and createdAt / lastUsedAt timestamps.
Profiles are listed by last use so frequent rotations stay at the top.
| Store | What it holds | Cleared when |
|---|---|---|
sessionStorage |
MSAL tokens, pending activation state during step-up, preferred tenant for the session | Tab closes |
localStorage |
Feature flags, theme, role cache, saved filters | Cleared by user / Disconnect flow |
| IndexedDB | Activation profiles | Cleared by user / browser tools |
| In-memory | Policy cache (30 min TTL), role render state, batch state | Page reloads |
staticwebapp.config.json defines:
- A SPA fallback that rewrites unmatched routes to
/index.html. - A strict Content Security Policy (see Security and Privacy for the full breakdown).
- Cache-control headers that keep
/js/*,/css/*,/favicons/*, and/manifest.jsonfresh.
A backend would need to store or proxy access tokens, which fundamentally changes the security model. Keeping every privileged call browser-direct means:
- No second place where tokens could leak.
- No data residency questions — your tokens never leave Microsoft's auth surface plus your browser.
- No service to run, scale, or patch.
- Self-hosting collapses to "drop the static files into Azure Static Web Apps" with a Bicep template.