Skip to content

Add end-to-end trigger-config validation before typed callback deserialization #217

Description

@daviburg

Description

Add a framework-neutral end-to-end trigger configuration validation path to ConnectorTriggerPayload.

A callback's HTTP headers identify the Connector Namespace resource and trigger-config resource that delivered it. They do not identify the connector API name or Swagger trigger operation. Before deserializing a caller-selected TriggerCallbackPayload<T>, the SDK must use that resource identity to retrieve the authoritative Connector Namespace trigger configuration, then compare the configuration's connector and operation to the expected typed payload identity.

The existing body-only APIs stay unchanged:

ConnectorTriggerPayload.Read<TPayload>(string json)
ConnectorTriggerPayload.ReadAsync<TPayload>(Stream body, ...)

They remain appropriate when the application intentionally owns all routing and type selection.

Required end-to-end flow

For each callback that uses the validating overload:

  1. The application adapts its host request into a BCL-only transport: Stream Body plus IReadOnlyDictionary<string, IEnumerable<string>> Headers. Azure.Connectors.Sdk must not reference Azure Functions or ASP.NET Core types.

  2. Parse the resource-context headers emitted by Connector Namespace webhook delivery:

    • x-ms-subscription-id
    • x-ms-resource-group
    • x-ms-gateway-resource-name
    • x-ms-trigger-name
  3. Treat those values as a ConnectorNamespaceTriggerConfigResourceIdentity:
    subscriptionId, resourceGroupName, connectorNamespaceName, triggerConfigName.
    They are resource names, not a connector API name or operation name.

  4. Use an SDK-provided, authenticated Connector Namespace management client to GET:

    /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}
      /providers/Microsoft.Web/connectorGateways/{connectorNamespaceName}
      /triggerconfigs/{triggerConfigName}?api-version={configured-version}
    

    The management endpoint, token credential, API version, and cloud/audience must be configurable. Use the existing SDK TokenCredential pattern; do not bind this client to Functions or ASP.NET Core.

  5. Validate that the returned trigger configuration has:

    • properties.connectionDetails.connectorName equal to the expected generated connector API name, and
    • properties.operationName equal to the expected generated trigger-operation constant.
  6. Only after both comparisons succeed, delegate to the existing bounded ReadAsync<TPayload>(Stream, ...) deserializer.

A generic client must never infer typed payload identity from x-ms-gateway-resource-name or x-ms-trigger-name; those are the Connector Namespace and trigger-config resource names.

Proposed API shape

Exact naming may evolve, but preserve these responsibilities and host boundaries:

public sealed record ConnectorNamespaceTriggerConfigResourceIdentity(
    string SubscriptionId,
    string ResourceGroupName,
    string ConnectorNamespaceName,
    string TriggerConfigName);

public sealed record ConnectorTriggerIdentity(
    string ConnectorName,
    string OperationName);

public sealed record ConnectorNamespaceTriggerConfig(
    string ConnectorName,
    string OperationName);

public interface IConnectorNamespaceTriggerConfigResolver
{
    ValueTask<ConnectorNamespaceTriggerConfig> GetTriggerConfigAsync(
        ConnectorNamespaceTriggerConfigResourceIdentity resourceIdentity,
        CancellationToken cancellationToken = default);
}

public static ValueTask<TPayload?> ReadAsync<TPayload>(
    ConnectorTriggerTransport transport,
    ConnectorTriggerIdentity expectedIdentity,
    IConnectorNamespaceTriggerConfigResolver triggerConfigResolver,
    long maxBodySizeBytes = ConnectorTriggerPayload.DefaultMaxBodySizeBytes,
    CancellationToken cancellationToken = default)
    where TPayload : class;

The SDK must also provide the standard resolver implementation backed by the Connector Namespace management API and TokenCredential; the resolver interface allows applications to use a test double or a cache without coupling ConnectorTriggerPayload to a particular HTTP host.

The first implementation should perform an authoritative GET for each validated callback. Do not add a transparent cache unless it has explicit configuration, bounded TTL/ETag behavior, safe invalidation on trigger-config updates, and tests proving a changed connector/operation cannot be accepted from stale cache state.

Error behavior and safe diagnostics

Use distinct exception categories so callers can troubleshoot without treating all failures as malformed payloads:

  • Missing/malformed resource-context headers: ConnectorTriggerResourceIdentityException.
  • Trigger config not found, unauthorized, API failure, or malformed GET response: ConnectorTriggerConfigurationResolutionException with safe status/resource diagnostics and the original exception as inner exception where appropriate.
  • Resolved configuration connector/operation does not match the expected typed payload: ConnectorTriggerIdentityMismatchException containing expected and resolved connector/operation plus correlation ID when supplied.

Never include authorization headers, function keys, bearer tokens, callback URLs, raw payload content, lock tokens, or complete response bodies in exception messages/properties.

The SDK validation is defence in depth, not authentication. The host/service must authenticate the callback independently, and the application identity used for the trigger-config GET needs the least-privileged Connector Namespace trigger-config read permission.

Motivation

The current webhook callback sample chooses a concrete type from compile-time metadata:

[ConnectorTriggerMetadata(
    ConnectorName = ConnectorNames.Office365Outlook,
    OperationName = Office365TriggerOperations.OnNewEmail,
    Connection = "Connectors:Office365")]

var payload = await ConnectorTriggerPayload
    .ReadAsync<Office365OnNewEmailTriggerPayload>(request.Body, ...);

See:
https://github.com/Azure/Connectors-NET-Samples/blob/dc51818937c3dd8090fc5eb44d160dd35b3b3bcd/DirectConnector/Office365Functions.cs#L316-L336

The current service webhook generator supplies resource-context headers:
x-ms-subscription-id, x-ms-resource-group, x-ms-gateway-resource-name, and x-ms-trigger-name. The last two are the namespace and trigger-config resource names. The actual connector API name and Swagger trigger operation are persisted in the trigger config (connectionDetails.connectorName and operationName). Therefore, configuration GET is the required bridge between callback routing and a safe typed payload choice.

Webhook delivery is the only currently available trigger mode, so this is safe to add now. The poll-pull trigger design is under active review. Its future queue/receive contract should preserve the same resource identity and add durable per-message identity metadata, because pull messages can be queued, replayed, proxied, and processed outside the original HTTP response.

Non-goals

  • Do not change existing body-only Read<TPayload> / ReadAsync<TPayload>(Stream, ...) behavior.
  • Do not add Azure Functions, ASP.NET Core, or HttpRequestMessage dependencies to the core SDK.
  • Do not dynamically instantiate a generated payload type from a trigger config in this issue. Not every trigger has a JSON-deserializable typed payload; the generic TPayload remains caller-selected. This feature verifies that selection.
  • Do not treat resource-context header values as connector or operation metadata.
  • Do not introduce a pull-delivery client until its Connector Namespace response contract is finalized.

Alternatives considered

  1. Header-only connector/operation validation. Rejected. Existing headers identify resources, not connector/operation metadata, so it rejects valid callbacks or gives a false validation signal.
  2. Body-only deserialization. Remains supported through existing APIs, but cannot detect misrouted or incorrectly configured typed handlers.
  3. Host-specific request overloads. Rejected. Use a BCL-only transport adapter so Azure Functions isolated worker, ASP.NET Core, minimal APIs, and plain .NET hosts work without core package dependencies.
  4. Caching config automatically. Deferred. Correctness after trigger-config changes is more important than avoiding the initial GET; caching needs an explicit design.

Acceptance criteria

  • Existing body-only APIs remain unchanged and covered by regression tests.
  • A framework-neutral transport type and validating overload compile without Functions or ASP.NET Core references in Azure.Connectors.Sdk.
  • The SDK includes a configurable TokenCredential-based Connector Namespace trigger-config resolver/client that performs the authoritative GET.
  • The resource identity is built from all four callback headers and the management GET path is encoded safely.
  • Tests use realistic webhook headers where x-ms-gateway-resource-name and x-ms-trigger-name differ from connector API name and operation name.
  • Tests cover: matching resolved config; connector mismatch; operation mismatch; missing each resource header; config 404; authorization failure; resolver transport failure; malformed config response; cancellation; and assurance that sensitive values do not appear in exceptions.
  • Header-name comparisons are implemented case-insensitively inside the SDK, independent of the caller dictionary's comparer.
  • Documentation and the Functions sample show a host-local transport adapter plus resolver/client configuration.
  • The API version, management audience/cloud behavior, and least-privilege read permission are documented before GA.

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions