Skip to content

Commit a9a31c5

Browse files
authored
Simplify Python provider flow (#27)
2 parents a108d3d + 29ef14a commit a9a31c5

21 files changed

Lines changed: 1406 additions & 1837 deletions

‎.github/workflows/packages.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
$required = @{
7676
'epp-javascript.zip' = @('host.json', 'src/functions/SendOtp.js', 'node_modules/@azure/functions/package.json')
7777
'epp-dotnet-source.zip' = @('host.json', 'dotnet.csproj', 'Program.cs', 'Functions/SendOtp.cs', 'Src/PhoneProviderBase.cs')
78-
'epp-python-source.zip' = @('host.json', 'function_app.py', 'requirements.txt', 'src/dispatch.py')
78+
'epp-python-source.zip' = @('host.json', 'function_app.py', 'requirements.txt', 'src/jwe.py', 'src/provider.py')
7979
}
8080
$checksums = foreach ($name in ($required.Keys | Sort-Object)) {
8181
$path = Join-Path 'artifacts' $name

‎docs/CONTRACT.md‎

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ Per-request `providerCredentialElapsedMs` continues to measure the caller's reso
384384
- **Privacy**: never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads,
385385
raw exceptions, provider descriptions/responses or endpoint query strings. There is no plaintext diagnostic
386386
override. Each handler emits separate service events. JavaScript and .NET use fixed completion
387-
events rather than a mutable comprehensive summary; Python retains its safe summary.
387+
events rather than a mutable comprehensive summary; Python uses the same fixed-event approach
388+
through the standard `logging` pipeline.
388389
Generated Function IDs remain distinguished from raw Microsoft/provider support IDs. Original wire IDs
389390
and the required nonce echo remain unchanged. Support IDs can correlate customer activity; restrict
390391
log access and retention. Endpoint logs contain only scheme, host/port and API path, never userinfo,
@@ -407,20 +408,21 @@ Per-request `providerCredentialElapsedMs` continues to measure the caller's reso
407408

408409
### Application logs
409410

410-
JavaScript and Python emit JSON records with the shared fields described below. Service events have
411-
`logType: "service"` and an individual `eventName`; each invocation ends with a fixed
412-
`request_completed` event. JavaScript uses an immutable request context and does not build a mutable
413-
comprehensive request summary.
411+
JavaScript emits JSON service records with an immutable request context. Python uses the standard
412+
`logging` pipeline: `otp_log.py` defines fixed event IDs, names, levels and fields, while an immutable
413+
`LoggerAdapter` context supplies the Function and Microsoft trace identifiers. The configured
414+
logging provider owns Python output formatting and export. Neither runtime manually builds a mutable
415+
comprehensive request summary; each invocation ends with a fixed `request_completed` event.
414416

415-
.NET uses the standard `ILogger` pipeline instead of manually serializing JSON. `OtpLog` defines
416-
source-generated events with stable IDs and names, while `ILogger.BeginScope` supplies
417+
Likewise, .NET uses the standard `ILogger` pipeline instead of manually serializing JSON. `OtpLog`
418+
defines source-generated events with stable IDs and names, while `ILogger.BeginScope` supplies
417419
`FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`,
418420
`MsCorrelationId` and `MsCorrelationIdSource`. The configured logging provider owns output
419421
formatting and export. .NET emits `request_completed` as an ordinary typed event rather than a
420422
mutable comprehensive summary. JavaScript follows the same fixed-event model while retaining its
421423
existing JSON field names.
422424

423-
A successful .NET or JavaScript live request emits:
425+
A successful live request in each runtime emits:
424426

425427
`request_received`, `payload_validated` (`envelope_validated` in JavaScript),
426428
`delivery_context_decrypted`, `provider_selected`,
@@ -441,12 +443,12 @@ bodies, decrypted delivery fields, credentials, provider response bodies, query
441443
exception messages. Endpoint values contain only scheme, host/port and path. Evaluation omits all
442444
provider events and emits `evaluation_completed`.
443445

444-
A successful JavaScript live request emits these separate service events:
446+
A successful live request emits these separate service events:
445447

446448
| Service event | Safe information recorded |
447449
|---|---|
448450
| `request_received` | Function invocation and available raw Microsoft trace IDs under their `x-ms-*` names; no raw body or arbitrary headers. |
449-
| `envelope_validated` | Allowlisted body metadata: validated `envelopeType`, normalized `channel`, `evaluation`, optional `ttlSeconds`, and `encryptedDeliveryContextPresent: true`. |
451+
| `envelope_validated` / `payload_validated` | Allowlisted body metadata: validated payload type, normalized `channel`, `evaluation` and optional `ttlSeconds`. |
450452
| `delivery_context_decrypted` | Decryption completed; no plaintext fields, JWE or key ID. |
451453
| `provider_selected` | Registered provider and its authentication mode. |
452454
| `provider_credential_resolution_started` | OAuth client-assertion or Key Vault credential source, with explicitly named raw OAuth application/identity/tenant IDs. |
@@ -455,7 +457,7 @@ A successful JavaScript live request emits these separate service events:
455457
| `provider_request_built` | Allowlisted HTTP method, final endpoint scheme/host/port/API path, HTTPS and disabled redirects; no query string, authorization headers or body. |
456458
| `provider_request_started` | The outbound send is beginning, with method, sanitized endpoint and timeout. |
457459
| `provider_response_received` | Actual upstream HTTP status; emitted before response-body reading completes. |
458-
| `provider_response_processed` | Mapped provider status/outcome, raw provider message/reference ID, duration and resulting Function HTTP status. |
460+
| `provider_response_processed` | Mapped provider status/outcome, fixed failure classification and duration. Raw provider descriptions and bodies are excluded. |
459461
| `response_prepared` | Response status and booleans indicating nonce/correlation inclusion, not their values or the response body. |
460462

461463
Body metadata is built from validated fields, **not** from a body dump with a few sensitive
@@ -518,31 +520,13 @@ These are tracing fields, not authentication assertions. In particular, an incom
518520
does not become a trusted tenant identity in logs. The existing wire correlation precedence,
519521
provider request IDs and public responses are unchanged.
520522

521-
Python retains a comprehensive request summary. JavaScript emits the same safe concepts only on the
522-
fixed event where each value is known; its `request_completed` event contains the final HTTP status,
523-
result and elapsed time rather than a cumulative mutable snapshot:
524-
525-
| Fields | Purpose |
526-
|---|---|
527-
| `httpStatus`, `result`, `elapsedMs` | Final Function response, `accepted` / `evaluated` / `failed`, and total handler time in milliseconds. Acceptance is not handset delivery. |
528-
| `envelopeType`, `channel`, `evaluation`, `ttlSeconds` | Allowlisted request-body metadata; null until envelope validation succeeds. Omitted TTL remains null; logging does not introduce expiry enforcement. |
529-
| `providerName`, `providerAuthMode`, `providerAttempted` | Fixed provider ID, its `apiKey` / `oauth` mode, and whether provider HTTP was attempted. Unknown configured names and credentials are never echoed. |
530-
| `providerCredentialSource`, `providerCredentialElapsedMs` | Credential resolution path and duration, including failed resolution; null if it never started. |
531-
| `providerTenantId`, `functionOutboundClientId`, `functionOutboundManagedIdentityClientId` | Raw configured OAuth identity IDs; null when OAuth resolution was not attempted. |
532-
| `providerHttpMethod`, `providerEndpoint` | Final provider request method and scheme/host/port/API path, set only after request construction and URL validation. No query string. |
533-
| `providerHttpStatus`, `providerStatus`, `providerOutcome` | Actual upstream HTTP status and normalized result. Status is logged only when the provider recognized it; otherwise it is `unmapped`. |
534-
| `providerMessageId` | Raw provider lookup/reference ID for support escalation. |
535-
| `providerElapsedMs`, `providerTimeoutMs` | Outbound request duration including response-body reading, and the configured/clamped HTTP timeout. Neither is an end-to-end deadline. |
536-
| `failureStage`, `failureReason` | Stage and fixed diagnostic reason, such as `provider_credentials` / `credential_unavailable`, `provider_transport` / `provider_timeout`, or `provider_response` / `provider_rejected`. No exception messages. |
537-
| `encryptionKeyIdMismatch` | Whether the advisory warning was emitted; never the configured or received key ID. |
538-
| `responseContainsNonce`, `responseContainsCorrelationId` | Whether those fields are in the prepared response, without recording their values. Null if no response was prepared. |
539-
| `omittedIdFields` | Names of support ID fields whose current values failed the logging format/length guard; empty for ordinary valid IDs. |
540-
541-
Provider fields remain null when their stage was not reached. `providerHttpStatus` is captured as
542-
soon as headers arrive, so a response-body timeout can legitimately show upstream `200` alongside
543-
Function `httpStatus: 504`, without a mapped provider status or success acknowledgement. Unknown
544-
provider status text and malformed JSON are never logged; malformed JSON emits only
545-
`provider_response_invalid_json` before the provider outcome rules run.
523+
The fixed `request_completed` event contains only the final HTTP status, result and elapsed time.
524+
Stage-specific fields remain on the event where they become known instead of being accumulated into
525+
a mutable summary. `providerHttpStatus` is recorded when response headers arrive, so a response-body
526+
timeout can legitimately produce `provider_response_received` with upstream `200` followed by
527+
`request_failed` with Function status `504`, without a mapped provider status or success
528+
acknowledgement. Unknown provider status text and malformed JSON are never logged; malformed JSON
529+
emits only `provider_response_invalid_json` before the provider outcome rules run.
546530

547531
Normal events and completion events use Information; invalid requests, non-success 4xx outcomes and
548532
advisory warnings use Warning; 5xx failures and timeouts use Error. Keep application Information logs

‎package-python.ps1‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ try {
2323
New-Item -ItemType Directory -Path (Split-Path $destination) -Force | Out-Null
2424
Copy-Item -LiteralPath $file.FullName -Destination $destination
2525
}
26-
if (-not (Test-Path -LiteralPath (Join-Path $stage 'src/dispatch.py'))) { throw 'Missing Python application source.' }
26+
foreach ($name in @('function_app.py', 'src/jwe.py', 'src/provider.py')) {
27+
if (-not (Test-Path -LiteralPath (Join-Path $stage $name))) {
28+
throw "Missing Python application source: $name."
29+
}
30+
}
2731
$zip = Join-Path $temporary 'app.zip'
2832
[IO.Compression.ZipFile]::CreateFromDirectory($stage, $zip)
2933
New-Item -ItemType Directory -Path (Split-Path $archive) -Force | Out-Null

‎python/README.md‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# External Phone Provider Function: Python (v2 model)
22

3-
Implements the shared [contract](../docs/CONTRACT.md) with one dispatch engine and one selected
4-
provider per deployment. Target: Python 3.11, Azure Functions v4, Python v2 programming model.
3+
Implements the shared [contract](../docs/CONTRACT.md) with a direct Azure Function flow and one
4+
selected provider per deployment. Target: Python 3.11, Azure Functions v4, Python v2 programming model.
55

66
## Setup and deployment
77

88
1. Follow [customer onboarding](../docs/ONBOARDING.md). Set `EPP_PROVIDER_NAME` to the selected
9-
adapter's registered manifest id (`<adapter-id>` is only a placeholder).
10-
2. Consult the selected adapter and its manifest in [src/providers/](src/providers/) for required
11-
credentials and options. Store credentials in Key Vault under the declared secret names, grant
9+
provider id (`<adapter-id>` is only a placeholder).
10+
2. Consult the selected provider in [src/providers/](src/providers/) for required credentials and
11+
options. Store credentials in Key Vault under the provider-owned secret names, grant
1212
the Function's managed identity *Key Vault Secrets User*, and configure the matching endpoint/options.
1313
3. Base private local settings on [../docs/local.settings.sample.json](../docs/local.settings.sample.json),
1414
replacing placeholders and selecting `FUNCTIONS_WORKER_RUNTIME=python`. Put settings at the
@@ -49,7 +49,7 @@ For live delivery, add `EPP_PROVIDER_NAME`, the complete selected `EPP_PROVIDER_
4949
matching provider authentication settings to `Values`.
5050
Add `EPP_PROVIDER_ACCOUNT_NAME` and any adapter-specific options only when required. Keep values as
5151
strings, including optional `EPP_PROVIDER_TIMEOUT_MS: "1500"`. Replace placeholders; provider API
52-
keys belong in the manifest-named Key Vault secrets, not this file. See the
52+
keys belong in the provider-named Key Vault secrets, not this file. See the
5353
[complete variable table](../TECHNICAL.md#configure-environment-variables).
5454

5555
Core Tools loads `Values` into `os.environ`. Direct Python execution and pytest do not automatically
@@ -90,7 +90,7 @@ six-digit numeric run that is not part of a longer number and repeats the comple
9090

9191
## Source
9292

93-
Worker initialization selects `ApiKeyCache` or `AccessTokenCache` from the provider manifest's auth mode.
93+
Worker initialization selects `ApiKeyCache` or `AccessTokenCache` from the provider's credential specification.
9494
Only the selected cache starts: API keys use Key Vault and `cachetools.TTLCache`; access tokens use
9595
the MI/Entra SDKs without Key Vault. One daemon loop polls every 30 seconds. Configuration changes
9696
require restart. Callers can stop waiting without abandoning shared reads; synchronous SDK I/O uses connect/read
@@ -101,16 +101,17 @@ local evaluation without background credential acquisition.
101101

102102
| Source | Purpose |
103103
|---|---|
104-
| [function_app.py](function_app.py) | HTTP handler and adapter registration |
104+
| [function_app.py](function_app.py) | Typed request orchestration and direct provider selection |
105105
| [src/config.py](src/config.py) | Shared deployment settings |
106-
| [src/models.py](src/models.py) | Envelope, delivery-context, dispatch and normalized `ParsedResponse` dataclasses |
107-
| [src/dispatch.py](src/dispatch.py) | Boundary validation, JWE, provider registry and outcome mapping |
108-
| [src/credentials.py](src/credentials.py) | `ApiKeyCache`, `AccessTokenCache` and their shared refresh coordinator |
109-
| [src/request_log.py](src/request_log.py) | Request-scoped [service events and summaries](../docs/CONTRACT.md#application-logs) with explicit ID sources |
110-
| [src/providers/](src/providers/) | Adapter manifests and API-specific implementations |
106+
| [src/models.py](src/models.py) | Typed Entra payload, delivery context, provider request and result dataclasses |
107+
| [src/jwe.py](src/jwe.py) | Pinned JWE decryption and typed delivery-context conversion |
108+
| [src/provider.py](src/provider.py) | Shared HTTPS transport, timeout handling and endpoint status mapping |
109+
| [src/credentials.py](src/credentials.py) | `CredentialTokenService`, `ApiKeyCache` and `AccessTokenCache` |
110+
| [src/otp_log.py](src/otp_log.py) | Fixed standard-logging event definitions and immutable request context |
111+
| [src/providers/](src/providers/) | Provider-owned credentials, requests and response mapping |
111112
| [src/secrets.py](src/secrets.py) | Key Vault transport; bundle caching belongs to `ApiKeyCache` |
112113

113-
Add and register an adapter without adding provider-specific branches to the shared pipeline.
114-
Return `ParsedResponse` from `parse_response` using named fields; the engine reads attributes such as
115-
`parsed.provider_status_name`. Raw provider JSON remains local to the adapter, not a shared model hierarchy.
114+
Add a provider by subclassing `PhoneProviderBase`, declaring its credential specification, and
115+
implementing `build_request` and `map_response`. Return `ProviderResult` with the coarse endpoint
116+
outcome and a fixed safe failure classification. Raw provider JSON remains local to the provider.
116117
See [production limitations](../docs/CONTRACT.md#production-limitations) before production use.

0 commit comments

Comments
 (0)