Skip to content

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

Open
JLCode-tech wants to merge 10 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 10 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
…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

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