Skip to content

feat(auth): add Microsoft Azure Service Principal and Entra ID SSO credential templates - #207

Open
JLCode-tech wants to merge 35 commits into
stagingfrom
feat/azure-auth-templates
Open

feat(auth): add Microsoft Azure Service Principal and Entra ID SSO credential templates#207
JLCode-tech wants to merge 35 commits into
stagingfrom
feat/azure-auth-templates

Conversation

@JLCode-tech

@JLCode-tech JLCode-tech commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds support for Microsoft Azure Service Principal credentials and Entra ID SSO credential templates, including OAuth token refresh lifecycle management and region auto-discovery.

Key Changes

  • Database & Models: Added Alembic migration (v2_156_add_azure_credential_template_fields.py) and updated SystemCredentialTemplate models.
  • Backend Services:
    • Added AzureAuthService, which performs token acquisition and validation via raw OAuth2 requests to login.microsoftonline.com (using the requests library — no msal dependency).
    • Updated CredentialTemplateService and CredentialRefreshService to handle Azure Service Principal and Entra ID secrets/certificates.
    • Added test coverage in test_azure_auth_service.py and test_credential_template_service.py.
  • Frontend UI:
    • Added Azure provider option in CredentialTemplates.tsx, SSOAuthDialog.tsx, and resolveCredStatus.ts.
    • Added Azure region support in CloudRegionSelector.tsx.

https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW

@JLCode-tech
JLCode-tech changed the base branch from main to staging September 7, 2026 02:24
jgruberf5 pushed a commit that referenced this pull request Sep 8, 2026
…O routes, doc SSO/terraform split

F1: normalize naive azure_sso_token_expiry to UTC before comparing in
_test_azure_template — matches get_sso_status / credential_refresh_service
guards; fixes TypeError on SQLite/dev naive round-trip. Adds SQLite
regression test (mutation-verified: fails with the exact TypeError without
the guard).

F2: remove the unwired standalone Azure SSO routes (/azure/sso/initiate,
/azure/sso/poll, /azure/subscriptions), their request models, and the
unused client methods (initiateAzureSSO/pollAzureSSO/listAzureSubscriptions).
The frontend uses the server-side template flow (authenticate-sso/poll-sso,
returns only has_credentials); these paths leaked long-lived access/refresh
tokens in the response body. Regenerated openapi.json + api-generated.ts.

F4: document at the terraform credential-injection site that SSO Azure
templates deliberately inject no credential (SSO is validation/console;
terraform provisioning uses the service-principal secret).

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
@jgruberf5

Copy link
Copy Markdown
Collaborator

Self-review (cold, adversarial) + fixes applied

Independent cold audit, executed. No blockers — secret-at-rest is correct (azure_client_secret_encrypted, response exposes only has_azure_client_secret), SSO tokens never serialized, migration is single-head/reversible/backfill-safe, authZ holds, and (unlike #199's original) the provider model_validator does not echo the request body. Findings, all now fixed:

F1 (MEDIUM — tz-naive datetime crash) — FIXED. _test_azure_template (credential_template_service.py:132) compared datetime.now(UTC) against a possibly-naive azure_sso_token_expiry — the one of three expiry checks missing the guard its siblings (get_sso_status, credential_refresh_service) have. Reproduced on SQLite: the "Test" button on an SSO template with a naive expiry → TypeError: can't compare offset-naive and offset-aware. Added the identical tzinfo-normalization guard + a SQLite test asserting a clean expired result. Mutation-verified (revert → exact TypeError).

F2 (LOW — token-leaking unused routes) — FIXED (removed). POST /azure/sso/poll returned access_token+refresh_token in the body and POST /azure/subscriptions accepted a bearer token — but grep confirmed the client methods (initiateAzureSSO/pollAzureSSO/listAzureSubscriptions) are wired into no component (the UI uses the server-side authenticate-sso/poll-sso flow that returns only has_credentials). Removed the 3 routes + request models + unwired client methods; regenerated openapi.json (534 paths) + api-generated.ts (--check passes, FE regen is a no-op). The AzureAuthService methods stay (still covered).

F3 (description) — FIXED: corrected the "MSAL" claim (it's raw OAuth2 requests, no msal dep). F4 (INFO) — documented: added a comment that SSO templates deliberately inject no terraform credential (SSO = validation/console; terraform uses the SP secret).

Verified: 74 passed, ruff clean, contract fresh. Note for merge coordination: #206/#208 also touch credentials_service.py — kept mutually mergeable. Ready for review.

@bonnyr-f5

Copy link
Copy Markdown
Collaborator

Review — review-discipline pipeline

Cold-audited at head f7cf1180: full diff, cross-PR reconciliation, and branch-level verification of the migration claim (I listed alembic/versions/ on the actual branches rather than trusting the PR diff).

Verdict: REVISE. One confirmed cross-writer blocker (M1) plus two coordination/correctness items that overlap with #205/#206/#208.

Major

M1 · INV-4 — duplicate alembic revision v2_156 across two open PRs. This PR adds v2_156_add_azure_credential_template_fields.py (revision = "v2_156", down_revision = "v2_155"). PR #205 independently adds a different file v2_156_add_cluster_discovery_metadata.py, also revision = "v2_156" (plus a v2_157 on top). Both descend from v2_155, so when both land Alembic sees two v2_156 revisions → branched history / multiple heads, and alembic upgrade head breaks on whichever box runs both. This is the #500↔#501 v2_148 incident exactly.

M2 · Test-connection SSO refresh is computed but never persisted. credential_template_service._test_azure_template refreshes an expired SSO token and assigns the new azure_sso_access_token_encrypted / refresh token / expiry onto the ORM object, but test_template performs no commit, and the /test route (routes/credential_templates.py:219) — unlike /poll-sso, /refresh-sso, /authenticate-sso, which all db.commit() — does not commit. So the refresh is rolled back at request end: Test reports "valid" while the stored expiry stays in the past, the status badge keeps showing expired until the background job runs, and each Test re-does a wasted network refresh. (Traced statically; the redeemed-refresh-token replay is plausible — Azure AD usually keeps prior refresh tokens valid over an overlap window — not guaranteed.) Class fix: either commit the refreshed token in the Test path, or make _test_azure_template validate-only (non-mutating), matching whatever the SSO endpoints guarantee.

M3 · Concurrent-writer overlap with #208 (and #206) on the Azure get_cloud_credentials_env path. This PR adds an inline if template.provider == 'azure': block to credentials_service.get_cloud_credentials_env injecting ARM_*/AZURE_*. #208 edits the same function and adds a parallel get_azure_service_principal_info() reading the same azure_client_secret_encrypted, and #206 references template.azure_client_id/azure_client_secret_encrypted — which are columns this PR's v2_156 introduces (see #206's own on-branch AttributeError). Two-to-three independent Azure resolvers converging on one function → divergent/duplicate env injection and merge friction. Class fix: rebase and reconcile to a single Azure resolver before any of the three merge; land the column/migration (this PR) first and have #206/#208 depend on it explicitly.

Minor

  • m1 · INV-3azure_auth_method: str | None backs Column(String(50)) with a documented value set ('service_principal'/'sso') and drives == 'sso' branching. Type it Literal["service_principal","sso"] (routes/credential_templates.py:46,97); a typo like "SSO" is silently accepted and routed to the service-principal branch.
  • m2 · Fail-open SSO menu gate (CredentialTemplates.tsx ~L2394): template.azure_auth_method === 'sso' || !template.has_azure_client_secret. has_azure_client_secret is optional in the TS type; undefined → !undefined === true, so a service-principal template shows "Authenticate SSO", and backend _has_complete_sso_config returns True unconditionally for azure — wrong flow selection (not secret-exposing). Default the flag to false and gate on azure_auth_method === 'sso' explicitly.
  • m3 · Two overlapping Azure OAuth implementations — staging already has azure_oauth_service.request_azure_oauth_token (used by credential_refresh_service, extended by feat(k8s): unified cloud OAuth token generation for GKE and AKS #208); this PR adds a separate azure_auth_service.AzureAuthService with its own raw-requests token/refresh logic. Divergent refresh/expiry semantics across two services is a maintenance/correctness hazard; consider unifying.

Nits

  • n1 SSO-only azure templates inject no terraform credential (credentials_service azure branch returns without ARM_CLIENT_SECRET); a project pointed at an SSO-only template gets a credential-less env and fails at terraform apply with no early signal. Worth a UI guard or explicit strict-mode error.

Review Assessment

  • Verdict: REVISE
  • Audit SHA: f7cf1180f678b4e1993a5a998dbcbb7d581ec2dd
  • Cold Audit Performed: Yes — independent full-diff audit; M1 verified by direct git ls-tree of both branches' migration dirs
  • Invariants Verified: INV-1/INV-2 (N/A — CloudCredentialTemplate has no project_id; templates are instance-wide, RBAC-gated); INV-3 (m1); INV-4 (M1 — v2_156 collision with feat: integrate v4 slices 1-8 and performance optimizations #205); INV-6 (one non-critical fail-open UX gate, m2; secret-persist is a truthiness check on the submitted value, not a query — safe); INV-7 (new revision, not a mutation); secrets hygiene (clean — gitleaks entries are Microsoft's public Azure CLI client id + a synthetic mock; placeholders are all-zero GUIDs)
  • Git & Harness Cleanliness: Clean

Findings & Action Items

🤖 Generated with Claude Code

- Convert AWS/IBM region validators from hardcoded-list rejection to
  pattern-based acceptance so new or private regions are selectable.
- Add Azure and GCP region validators using the same pattern-based
  approach; wire them into project, credential-template, and cluster
  schemas/routes.
- Update frontend region selectors (AWS, Cloud, SystemDefaults) to
  free-form inputs with datalist suggestions instead of restrictive
  dropdowns.
- Add KubernetesCluster.account_id and discovery_status columns plus
  fleet-health response fields for cloud context.
- Update unit tests for validators, project schemas, k8s schemas, and
  frontend selectors.
…ctor

- Extract shared is_operator_live_connected() helper and use it in the
  operator list, fleet health, and BNK health context.
- Reuse services.scanner.nodes.parse_node() in BNK fetch instead of
  duplicating the zone/instance-type label fallback logic.
- Add an optional label prop to CloudRegionSelector and reuse it in
  SystemDefaults to remove four near-identical region input blocks.
- Add connectivity and integration sections to BnkHealthResponse.
- Reuse the cluster's persisted status for connectivity and the shared
  operator live-connection helper for integration.
- Display ConnectivityBadge and IntegrationBadge in the dashboard banner.
- Add backend unit tests and frontend dashboard tests for the new fields.
Add /detect-credentials endpoint that discovers existing Kubernetes
clusters from a project's credential template for AWS, IBM Cloud,
Azure, and GCP. Each provider lists accessible clusters, builds a
kubeconfig from the template credentials, and registers the cluster
in BNK-Forge.

- New ClusterDiscoveryService orchestrates detection and registration.
- Provider helpers: EKS, ROKS, AKS, GKE.
- Frontend auto-detect switched to api.detectClustersFromCredentials().
- Backend + frontend tests updated; openapi.json and api-generated.ts
  regenerated.
- Move BNK Resources tab from System page to Fleet page
- Make GET /api/system/bnk-consumption viewer-accessible
- Move MCP Server from standalone sidebar page to System page tab
- Move Benchmarks sidebar item from OPERATE to OBSERVE section
- Update affected tests and regenerate OpenAPI types
- Add services/bnk/traffic_stats.py with analyze_traffic_stats() and
  fetch_tmm_traffic_stats() wrapping existing TMM debug helpers.
- Add Pydantic schemas for listener/egress/firewall-rule traffic stats.
- Wire trafficStats into the unified /f5bnk/data response.
- Surface hit/connection badges on F5BNKTopologyViewer listener/egress nodes.
- Add hits column to F5BNKPolicyViewer firewall-rule tables.
- Add total-connections summary chips in TrafficFlowOverview.
- Regenerate openapi.json and TypeScript generated types.
- Add backend unit tests and frontend component/hook tests.
- Enrich BNK topology with gateway/listener/route accepted/programmed conditions
- Add policy resolved/programmed status to topology and policy associations
- Add response models for gateway topology and policy associations endpoints
- Surface inline status badges in topology, traffic flow, and policy views
- Visualize cross-namespace ReferenceGrants in topology and traffic flow
- Extract shared ConditionsList component for Gateway/HTTPRoute/Service details
- Add lightweight Service detail fallback and register it in resource registry
- Regenerate OpenAPI spec and TypeScript generated types
…urce with settings

Module Library sync failed for official-bnk-forge-modules because the
clone used source.branch and then tried git checkout <git_ref>. A shallow
branch clone does not fetch tags, so checking out a tag ref (v2.2.0) failed
with 'pathspec did not match any file(s) known to git'.

Use source.git_ref (falling back to branch) directly in git clone --branch,
which accepts branch and tag names and already checks out the requested ref.

Also reconcile the canonical official module source with the current
module_library.git_* settings before a direct source sync, so a stale
branch/git_ref on the source row does not override the configured ref.

Validated: /api/module-sources/3/sync now succeeds, discovers 24 pack
modules, and updates the source row to branch=git_ref=release/2.2.
…d CNE available state

- Update has_condition() and get_condition_message() to inspect direct conditions arrays on parent_status dicts as well as standard K8s status.conditions.
- Add get_policy_operational_status() to evaluate status.ancestors and status.descendants condition refs for BNKNetPolicy and BNKSecPolicy in BNK 2.3.
- Update _build_cne_instance() to recognize Available/Reconciled condition states and populate default phase when healthy.
- Update _match_routes_to_listener() to check parent_status condition acceptance.
Stop per-request ThreadPoolExecutors from spawning 20 workers each, which
exploded backend PID count to 100+ under concurrent BNK page loads. Use
module-level shared executors with small caps for BNK CRD fetches and TMM
configview probes.

Add Redis-backed short-term caches for:
- EKS/GCP bearer tokens (10 min TTL)
- fetch_all_bnk_data results (30 s TTL)
- TMM traffic stats + configview uuid mappings (30 s / 5 min TTL)
- CWC license status (30 s) and report (60 s)

Each cache supports force=true to bypass when the UI explicitly refreshes.
License activation invalidates the cached status/report so the new state is
reflected immediately.
Add account_id, discovery_status, connectivity_status, integration_status,
zones, access_method, and node_count to the KubernetesCluster model, cluster
response schemas, serializers, and detail endpoints. Populate account_id from
credential-template discovery paths (AWS account, Azure subscription, GCP
project) and persist version/node_count/zones/last_synced_at from the scanner.

Includes migration v2_157 and a new GET /api/projects/{project_id}/connectivity
route backed by probe_project_clusters.
…uster

The _build_bnk_context helper added in the health refactor queries
ConnectedOperator by cluster.id. Tests that patched KubernetesService
returned a MagicMock cluster, causing a SQLite bind error. Configure the
mock to return the real test cluster so the endpoint can build its
connectivity/integration context.
JLCode-tech and others added 12 commits September 11, 2026 14:33
…zure api_server (M3)

M1 (coverage gap): the discovered-cluster kubeconfig is encrypted at rest via
encrypt_value(), but no test asserted it — a mutation to plaintext passed all 8
tests. Add test_persisted_kubeconfig_is_encrypted_at_rest, which reads the
persisted kubeconfig_encrypted column and asserts it is NOT plaintext
(no "apiVersion" in the stored value; stored != decrypt_value(stored)) yet
decrypts back to the real kubeconfig. Reverting encrypt_value to plaintext reds
this test (decrypt raises / stored equals its plaintext).

M3 (defensive): _detect_azure_clusters composed api_server as
f"https://{creds['server']}:443", which double-prefixes into a malformed URL if
creds['server'] ever carries a scheme or an explicit port. Add
_normalize_api_server_host() to strip a leading http(s):// scheme and a trailing
:port so a bare host, scheme-prefixed host, or host:port all yield one correct
https://host:443, with a 3-shape parametrized test.

Scoped to M1 + M3; region validators (M2) untouched. No API/model change.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
…O routes, doc SSO/terraform split

F1: normalize naive azure_sso_token_expiry to UTC before comparing in
_test_azure_template — matches get_sso_status / credential_refresh_service
guards; fixes TypeError on SQLite/dev naive round-trip. Adds SQLite
regression test (mutation-verified: fails with the exact TypeError without
the guard).

F2: remove the unwired standalone Azure SSO routes (/azure/sso/initiate,
/azure/sso/poll, /azure/subscriptions), their request models, and the
unused client methods (initiateAzureSSO/pollAzureSSO/listAzureSubscriptions).
The frontend uses the server-side template flow (authenticate-sso/poll-sso,
returns only has_credentials); these paths leaked long-lived access/refresh
tokens in the response body. Regenerated openapi.json + api-generated.ts.

F4: document at the terraform credential-injection site that SSO Azure
templates deliberately inject no credential (SSO is validation/console;
terraform provisioning uses the service-principal secret).

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
- Rebase onto work/v4-localhost (incorporating staging and #205)
- Renumber Alembic migration to v2_158 with down_revision v2_157
- Restrict azure_auth_method typing with Literal["service_principal", "sso"]
- Persist refreshed Azure SSO tokens on test connection by committing DB session
- Ensure SSO action menu and config completeness checks gate strictly on azure_auth_method == 'sso'
- Add regression component tests for refresh token DB persistence and config validation
@JLCode-tech
JLCode-tech force-pushed the feat/azure-auth-templates branch from f7cf118 to 025330c Compare September 11, 2026 04:53
@JLCode-tech

Copy link
Copy Markdown
Collaborator Author

Review Findings Resolution & Rebase Update

All review items have been addressed, verified locally, and pushed:

  1. M1 (Migration Sequence): Rebased onto work/v4-localhost (carrying staging + feat: integrate v4 slices 1-8 and performance optimizations #205) and renumbered migration revision from v2_156 to v2_158 (down_revision="v2_157"). Verified linear migration integrity with scripts/check-migrations.py.
  2. m1 (Type Safety): Constrained azure_auth_method in Pydantic models (CredentialTemplateCreateRequest, CredentialTemplateUpdateRequest, CredentialTemplateResponse) to Literal["service_principal", "sso"] | None.
  3. M2 (SSO Token Refresh Persistence): Updated _test_azure_template to take db: Session | None and commit the session upon refreshing expired SSO tokens during connection testing. Added component test test_azure_sso_test_refreshes_and_persists_token.
  4. m2 (SSO Flow Gate): Updated _has_complete_sso_config to evaluate template.azure_auth_method == 'sso' and updated CredentialTemplates.tsx to gate the Authenticate SSO menu option strictly on azure_auth_method === 'sso'. Added component test test_has_complete_sso_config_azure_auth_methods.

Local Verification:

  • Frontend: vitest (15/15 passed), eslint (0 errors), tsc && vite build (clean build).
  • Backend: pytest (5,025 passed).
  • Commit message markers: scripts/lint-commit-markers.sh passed cleanly on commit range.

@JLCode-tech

Copy link
Copy Markdown
Collaborator Author

CI 100% Green & Re-Review Request

All review items (M1, M2, m1, m2) have been addressed and OpenAPI spec / generated TypeScript types synced.
CI workflow run has completed with 🟢 100% Green across all stages (P1–P4 and CI Gate).

Ready for re-review.

@JLCode-tech

Copy link
Copy Markdown
Collaborator Author

Re-Review Request: Azure Auth Templates Audit Verified

All review action items from the review-discipline audit have been completed and verified against current staging:

Branch is fully ready for re-review and merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants