Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
$required = @{
'epp-javascript.zip' = @('host.json', 'src/functions/SendOtp.js', 'node_modules/@azure/functions/package.json')
'epp-dotnet-source.zip' = @('host.json', 'dotnet.csproj', 'Program.cs', 'Functions/SendOtp.cs', 'Src/PhoneProviderBase.cs')
'epp-python-source.zip' = @('host.json', 'function_app.py', 'requirements.txt', 'src/dispatch.py')
'epp-python-source.zip' = @('host.json', 'function_app.py', 'requirements.txt', 'src/jwe.py', 'src/provider.py')
}
$checksums = foreach ($name in ($required.Keys | Sort-Object)) {
$path = Join-Path 'artifacts' $name
Expand Down
56 changes: 20 additions & 36 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ Per-request `providerCredentialElapsedMs` continues to measure the caller's reso
- **Privacy**: never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads,
raw exceptions, provider descriptions/responses or endpoint query strings. There is no plaintext diagnostic
override. Each handler emits separate service events. JavaScript and .NET use fixed completion
events rather than a mutable comprehensive summary; Python retains its safe summary.
events rather than a mutable comprehensive summary; Python uses the same fixed-event approach
through the standard `logging` pipeline.
Generated Function IDs remain distinguished from raw Microsoft/provider support IDs. Original wire IDs
and the required nonce echo remain unchanged. Support IDs can correlate customer activity; restrict
log access and retention. Endpoint logs contain only scheme, host/port and API path, never userinfo,
Expand All @@ -407,20 +408,21 @@ Per-request `providerCredentialElapsedMs` continues to measure the caller's reso

### Application logs

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

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

A successful .NET or JavaScript live request emits:
A successful live request in each runtime emits:

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

A successful JavaScript live request emits these separate service events:
A successful live request emits these separate service events:

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

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

Python retains a comprehensive request summary. JavaScript emits the same safe concepts only on the
fixed event where each value is known; its `request_completed` event contains the final HTTP status,
result and elapsed time rather than a cumulative mutable snapshot:

| Fields | Purpose |
|---|---|
| `httpStatus`, `result`, `elapsedMs` | Final Function response, `accepted` / `evaluated` / `failed`, and total handler time in milliseconds. Acceptance is not handset delivery. |
| `envelopeType`, `channel`, `evaluation`, `ttlSeconds` | Allowlisted request-body metadata; null until envelope validation succeeds. Omitted TTL remains null; logging does not introduce expiry enforcement. |
| `providerName`, `providerAuthMode`, `providerAttempted` | Fixed provider ID, its `apiKey` / `oauth` mode, and whether provider HTTP was attempted. Unknown configured names and credentials are never echoed. |
| `providerCredentialSource`, `providerCredentialElapsedMs` | Credential resolution path and duration, including failed resolution; null if it never started. |
| `providerTenantId`, `functionOutboundClientId`, `functionOutboundManagedIdentityClientId` | Raw configured OAuth identity IDs; null when OAuth resolution was not attempted. |
| `providerHttpMethod`, `providerEndpoint` | Final provider request method and scheme/host/port/API path, set only after request construction and URL validation. No query string. |
| `providerHttpStatus`, `providerStatus`, `providerOutcome` | Actual upstream HTTP status and normalized result. Status is logged only when the provider recognized it; otherwise it is `unmapped`. |
| `providerMessageId` | Raw provider lookup/reference ID for support escalation. |
| `providerElapsedMs`, `providerTimeoutMs` | Outbound request duration including response-body reading, and the configured/clamped HTTP timeout. Neither is an end-to-end deadline. |
| `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. |
| `encryptionKeyIdMismatch` | Whether the advisory warning was emitted; never the configured or received key ID. |
| `responseContainsNonce`, `responseContainsCorrelationId` | Whether those fields are in the prepared response, without recording their values. Null if no response was prepared. |
| `omittedIdFields` | Names of support ID fields whose current values failed the logging format/length guard; empty for ordinary valid IDs. |

Provider fields remain null when their stage was not reached. `providerHttpStatus` is captured as
soon as headers arrive, so a response-body timeout can legitimately show upstream `200` alongside
Function `httpStatus: 504`, without a mapped provider status or success acknowledgement. Unknown
provider status text and malformed JSON are never logged; malformed JSON emits only
`provider_response_invalid_json` before the provider outcome rules run.
The fixed `request_completed` event contains only the final HTTP status, result and elapsed time.
Stage-specific fields remain on the event where they become known instead of being accumulated into
a mutable summary. `providerHttpStatus` is recorded when response headers arrive, so a response-body
timeout can legitimately produce `provider_response_received` with upstream `200` followed by
`request_failed` with Function status `504`, without a mapped provider status or success
acknowledgement. Unknown provider status text and malformed JSON are never logged; malformed JSON
emits only `provider_response_invalid_json` before the provider outcome rules run.

Normal events and completion events use Information; invalid requests, non-success 4xx outcomes and
advisory warnings use Warning; 5xx failures and timeouts use Error. Keep application Information logs
Expand Down
6 changes: 5 additions & 1 deletion package-python.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ try {
New-Item -ItemType Directory -Path (Split-Path $destination) -Force | Out-Null
Copy-Item -LiteralPath $file.FullName -Destination $destination
}
if (-not (Test-Path -LiteralPath (Join-Path $stage 'src/dispatch.py'))) { throw 'Missing Python application source.' }
foreach ($name in @('function_app.py', 'src/jwe.py', 'src/provider.py')) {
if (-not (Test-Path -LiteralPath (Join-Path $stage $name))) {
throw "Missing Python application source: $name."
}
}
$zip = Join-Path $temporary 'app.zip'
[IO.Compression.ZipFile]::CreateFromDirectory($stage, $zip)
New-Item -ItemType Directory -Path (Split-Path $archive) -Force | Out-Null
Expand Down
33 changes: 17 additions & 16 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# External Phone Provider Function: Python (v2 model)

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

## Setup and deployment

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

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

## Source

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

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

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