Portal Runtime¶
The portal backend is no longer a monolithic server.py. The runtime now lives under portal/app/ and portal/server.py is a thin compatibility wrapper and CLI entrypoint.
Package Layout¶
| Module | Purpose |
|---|---|
main.py |
FastAPI app factory, middleware, lifespan |
settings.py |
local and cloud settings validation |
auth.py |
Entra auth and role dependencies |
routers/ |
public and authenticated route registration |
clients/ |
admin-control-plane, agent, and Graph clients |
services/ |
policy, scan, CA, and health orchestration |
storage/ |
file and Blob-backed policy config persistence |
Runtime Model¶
The app builds a single container object and stores it on app.state.
That container owns:
- settings
- pooled HTTP clients
- auth validator
- policy config store
- services
This replaced the old pattern of globals and ad-hoc caches inside one large file.
Health Model¶
The portal exposes three different health surfaces on purpose:
| Route | Meaning |
|---|---|
/healthz/live |
process is running |
/healthz/ready |
required dependencies are ready |
/api/system-status |
authenticated dependency-level operational detail |
That split keeps readiness honest without overloading the public liveness contract.
Cloud Versus Local¶
Local mode:
- may run without Entra auth
- uses
portal/portal-config.json - stores saved policy configs in
portal/policy-configs.json
Cloud mode:
- requires admin-control-plane URL and management key
- requires Entra auth settings
- discovers agents from
admin-control-plane - stores policy configs in Azure Blob Storage through managed identity
Storage Contract¶
Policy config persistence is intentionally different by environment:
- local development uses the file-backed store
- cloud runtime uses Blob storage for durability across revisions and restarts
The portal Container App managed identity must have Blob data permissions on the portal policy store.
Request Handling Notes¶
- request IDs flow through to downstream dependency calls
- mutation routes are admin-only
- Graph failures are surfaced as real errors, not fabricated healthy state
- cloud startup should fail fast when required runtime configuration is missing
Risk Settings¶
The Settings tab separates portal risk monitoring from gateway Entra risk enforcement:
- Portal monitoring defaults to On and reads Graph
identityProtection/riskyAgents. The sidebar explicitly labels this as PORTAL RISK MONITORING: On after a successful read, Off after an explicit opt-out, and Unavailable on licensing, authorization, or network failures. Successful empty responses do not establish low risk for absent agents. This preference does not stop runtime lookups when gateway enforcement is enabled, or change the configured provider for manual risk-update actions. - Settings uses the portal's shared card, badge, and notification styles with keyboard-accessible switches. Provider failures show customer-facing licensing, permission, or availability messages; raw Graph responses and request IDs remain in server diagnostics, not Settings or sidebar tooltips.
- Monitoring, enforcement, and cache lifetime are stored separately from named policy presets in
portal-runtime-settings/settings.jsonusing the existing managed identity, or the equivalent local file. Invalid or unreadable stored settings produce an error, not a silent fallback. The settings API reconciles supported gateway settings to saved preferences after restart. - Blob updates use the public
azure.core.MatchConditionsAPI for conditional ETag writes. Storage regression tests exercise the real SDK upload path; browser matrix casebrowser.management.local.risk-settingscovers both switches and persistence across reload. - Local enforcement is controlled by
admin_governance.risk_enforcementin the active sidecar policy. Only the explicit valueoffskips local risk checks. Missing or other values retain existing fail-closed checks. Agent-disabled, tag, mTLS, RBAC, and JWT controls remain independent. - Enabling gateway enforcement requires a ready CA cache and permitted Entra control-plane risk evidence to prevent immediate management lockout. An old sidecar or manually supplied low rating cannot satisfy this preflight.
- Both built-in demo presets and the shipped demo policy default to
offand omit the deprecated per-caller blocked risk levels. Hydrated presets respect an operator's persisted enforcement setting. This does not alter live tenant CA policies or token-issuance enforcement. - Production gateways query Entra directly for the authenticated caller's Agent Identity service principal object ID. They do not depend on portal polling or copy portal results into a manual store. The default maximum rating age is 90 seconds, configured by
admin_governance.risk_cache_seconds; 0 performs a fresh lookup for every governed call. Expiry triggers a fresh lookup on the next call. Failures invalidate cached evidence, and missing/invalid/404 responses are unavailable, not safe. Explicit Entranoneis valid evidence. SOC/manual evidence can only increase risk, never override Entra high risk or substitute for unavailable Entra evidence. Durable manual evidence remains tracked in #48. - Info buttons explain each control and cache tradeoffs on hover or keyboard focus. Disabling enforcement skips only gateway risk checks; other configured security layers and tenant token-time policies remain independent.
GET /api/settings/risk is viewer/admin readable. The signal and enforcement PUT routes accept {"enabled": true} or {"enabled": false}. PUT /api/settings/risk-cache accepts a non-negative integer {"seconds": 90}, including zero. All writes require administrator authorization. Cache changes and enabling runtime enforcement require an upgraded sidecar advertising entra_risk_enforcement_supported. Both preset hydration and application preserve the saved cache lifetime. Settings changes restore the previous saved preference if gateway updates fail, and report failed rollback explicitly.
This feature includes a sidecar change; do not deploy it with --portal-only until the sidecar has been upgraded through the normal deployment/attestation flow.
For an already-bootstrapped environment where tenant CA policies must remain unchanged, use SKIP_ENTRA=true ./deploy.sh --skip-provision to upgrade workloads and re-attest without running Entra provisioning.
infra/modules/portal-support.bicep provisions the private runtime settings container inside the environment-owned storage account. deploy.sh initializes its blob from portal/default-risk-settings.json only if it does not exist; regular and portal-only deployments preserve operator choices. scripts/teardown.sh deletes the storage account and settings through azd down --force --purge; scoped rebuild deletes the resource group, including settings, before provisioning new defaults. --skip-azd performs environment-variable cleanup only and deliberately does not delete live settings.