Skip to content

feat(directory): Phase 1 backend — Directory layer + Entra provider#406

Open
larsgeorge-db wants to merge 1 commit into
mainfrom
feat/directory-backend
Open

feat(directory): Phase 1 backend — Directory layer + Entra provider#406
larsgeorge-db wants to merge 1 commit into
mainfrom
feat/directory-backend

Conversation

@larsgeorge-db
Copy link
Copy Markdown
Collaborator

First of two PRs implementing Phase 1 of the directory-lookup-and-principal-picker plan (#375). This PR is the backend foundation; a follow-up PR will add the Settings tab + PrincipalPicker + Assign Owner migration.

Source PRD: docs/prds/prd-entra-id-graph-integration.md (#335)
Plan: plans/directory-lookup-and-principal-picker.md (#375)

Summary

  • New Directory layer: provider-agnostic abstraction (DirectoryProvider ABC) with a single concrete provider for Microsoft Entra ID. All Graph traffic goes through ws.serving_endpoints.http_request(connection_name=...) so UC owns OAuth2 client-credentials acquisition and token caching — the app stores no secret.
  • DirectoryManager dispatches to the configured provider and caches results in a 5-min in-memory TTL keyed on (provider_type, connection_name, kind, query|limit), invalidating on settings change or explicit invalidate_cache().
  • Settings live in the existing app_settings key/value table (DIRECTORY_PROVIDER_TYPE, DIRECTORY_UC_HTTP_CONNECTION_NAME) — no Alembic migration.
  • Five routes under /api/directory/*: status, search, test, settings, uc-http-connections. Permission-gated against the existing settings feature.
  • Shared list_http_connections() helper extracted from workflows_routes.py; the existing /api/workflows/http-connections route now delegates.
  • Test conftest gains non-secret setdefaults for required Settings fields so the suite is runnable locally without .env.

What's intentionally not here

  • Frontend (PrincipalPicker, Settings → Directory tab, Assign Owner migration) — next PR.
  • Production second provider — the abstraction is exercised by a unit-test stub. Okta / Ping land in a future phase per plan's out-of-scope list.
  • Storage-shape changes — every existing principal-bearing field keeps its string / List[str] shape.

Architectural notes

  • Principal.id is the persisted identifier (UPN/email for users, displayName for groups — not GUIDs). sub_label carries the secondary identifier (email/UPN or GUID) for disambiguation.
  • Unknown provider types in settings are treated as not-configured. UI uses DirectoryStatus.configured to decide picker mode.
  • All search responses are de-duplicated by (type, id) to survive partial cache hits and the cross-type merge honours the caller's overall limit.

Test plan

  • Backend unit suite: cd src && hatch -e dev run pytest backend/src/tests/unit/764 passed, 0 failed
  • New tests:
    • test_entra_id_provider.py (16): OData escaping, $select projections, Principal mapping (UPN → id for users, displayName → id for groups), eventual-consistency header, response body shapes (bytes / str / stream / None), Graph error body surfaced as DirectoryError, transport-error surfaces, non-JSON body, empty connection-name guard.
    • test_directory_manager.py (14): unknown-provider-type stays not-configured, dispatches to registered provider, cache hits on case + whitespace normalisation, cache invalidates when settings change or invalidate_cache() is called, types=["user"] narrows downstream calls, stub-provider registration proves the abstraction is enough to add a provider with no manager/route changes.
  • Lint clean for new modules (ruff check --isolated).
  • Manual smoke test of /api/directory/status against a workspace (left for reviewer or follow-up PR alongside the UI).

Adds the provider-agnostic Directory layer (manager + provider plug-in
interface + EntraIdProvider) and four routes under /api/directory.

Backend pieces:
- DirectoryProvider ABC + EntraIdProvider that calls Microsoft Graph
  exclusively through ws.serving_endpoints.http_request(connection_name=...).
  UC owns OAuth2 client-credentials and token caching; the app stores
  no client secret. Includes OData escaping, eventual-consistency
  header for startswith filters, and tight $select projections.
- DirectoryManager dispatches to the configured provider, holds a
  5-minute in-memory TTL cache keyed on (provider_type, connection_name,
  kind, query|limit), and invalidates on provider/connection change or
  explicit settings update.
- Pydantic models: Principal (user|group|unknown, id=UPN/displayName,
  display_name, sub_label), DirectoryStatus, DirectoryTestResult,
  DirectorySearchResponse, DirectorySettingsUpdate. Settings keys live
  in app_settings (no Alembic migration): DIRECTORY_PROVIDER_TYPE,
  DIRECTORY_UC_HTTP_CONNECTION_NAME.
- Routes (settings read/write permission-gated):
  GET  /api/directory/status
  GET  /api/directory/search?q=&types=&limit=
  POST /api/directory/test
  PUT  /api/directory/settings
  GET  /api/directory/uc-http-connections
- Shared list_http_connections() helper extracted from workflows route;
  the existing /api/workflows/http-connections endpoint now delegates.
- Manager wired into startup_tasks + DI getter, exposed as
  DirectoryManagerDep.

Tests:
- 16 EntraIdProvider tests: OData escaping, $select projections,
  Principal mapping (UPN -> id for users, displayName -> id for
  groups), eventual-consistency header, response body shapes (bytes,
  str, stream, None), Graph error body, transport errors, non-JSON.
- 14 DirectoryManager tests: configured-status edge cases (unknown
  provider type stays not-configured), cache hit (case + whitespace
  normalisation), cache invalidation when settings change OR when
  invalidate_cache() called, types filter narrows downstream calls,
  stub-provider registration proves the abstraction is enough to add
  a new provider with no manager/route changes.
- Backend test conftest now sets non-secret defaults for
  DATABRICKS_HOST / DATABRICKS_WAREHOUSE_ID / APP_AUDIT_LOG_DIR via
  setdefault so the suite runs without a local .env.

Plan: plans/directory-lookup-and-principal-picker.md (PR #375).
larsgeorge-db added a commit that referenced this pull request May 21, 2026
…r scope

Documents what shipped under PRs #406 / #407 / #412 / #413 / #416 / #417:

- Renames the integration's manager / routes / settings keys in the
  PRD to match the implementation (Directory layer, /api/directory/*,
  DIRECTORY_* settings, Settings → Directory tab).
- Documents the DirectoryProvider interface and the
  (DirectoryProviderContext, DirectoryProviderConfig) factory
  signature so future provider plug-ins know what to implement.
- Documents the v1 provider set, which expanded during planning
  from Entra-only to entra + lakebase + file. The Lakebase table
  schema and CSV format are included so operators have a single
  reference.
- Preserves story content, the disambiguation rule, both picker
  modes, storage-compatibility guarantees, and graceful-degradation
  rules from the PRD body unchanged.
- Re-confirms the out-of-scope list (Okta/Ping, service principals,
  OBO, profile photos, manager hierarchy, role/team Select replacement,
  CSV bulk import) which the abstraction makes cheap to revisit.
larsgeorge-db added a commit that referenced this pull request May 21, 2026
…r scope

Documents what shipped under PRs #406 / #407 / #412 / #413 / #416 / #417:

- Renames the integration's manager / routes / settings keys in the
  PRD to match the implementation (Directory layer, /api/directory/*,
  DIRECTORY_* settings, Settings → Directory tab).
- Documents the DirectoryProvider interface and the
  (DirectoryProviderContext, DirectoryProviderConfig) factory
  signature so future provider plug-ins know what to implement.
- Documents the v1 provider set, which expanded during planning
  from Entra-only to entra + lakebase + file. The Lakebase table
  schema and CSV format are included so operators have a single
  reference.
- Preserves story content, the disambiguation rule, both picker
  modes, storage-compatibility guarantees, and graceful-degradation
  rules from the PRD body unchanged.
- Re-confirms the out-of-scope list (Okta/Ping, service principals,
  OBO, profile photos, manager hierarchy, role/team Select replacement,
  CSV bulk import) which the abstraction makes cheap to revisit.
@larsgeorge-db larsgeorge-db force-pushed the feat/directory-backend branch from b2ee317 to d2f09fa Compare May 21, 2026 21:35
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.

1 participant