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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Support configurable scheduler token audiences and government defaults ([#806](https://github.com/microsoft/durabletask-dotnet/pull/806))

## v1.26.0
- Adding rewind to the sidecar by sophiatev ([#802](https://github.com/microsoft/durabletask-dotnet/pull/802))
- Prevent external-event loss after canceled waits in isolated worker by wangbill ([#801](https://github.com/microsoft/durabletask-dotnet/pull/801))
Expand Down
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,104 @@ For runnable DTS emulator examples that demonstrate versioning, see the [WorkerV

The [on-demand sandbox activities sample](samples/on-demand-sandbox/README.md) shows how to declare selected activities for Durable Task Scheduler (DTS)-managed on-demand sandbox execution and build the remote worker container image separately from the declarer app.

### Token audiences and Azure Government

`DurableTaskSchedulerClientOptions.ResourceId` and `DurableTaskSchedulerWorkerOptions.ResourceId`
configure the **token audience URI**, not an Azure Resource Manager resource path.
The same setting is available as `ResourceId` in a scheduler connection string, for every
authentication type. A `UseDurableTaskScheduler` configuration callback can override the
connection-string value.

| Configuration | Selected audience |
| --- | --- |
| Explicit nonempty `ResourceId` | The normalized explicit value |
| Missing, null, or empty `ResourceId`, with `REGION_NAME` starting with `usgov` or `usdod` (case-insensitive) | `https://durabletask.azure.us` |
| Otherwise | `https://durabletask.io` |

The default is resolved per options instance and retained across token refreshes and channel
recreation. Region matching uses prefixes only: `chinaeast2`, `notusgov`, and `notusdod` use
the public default. The audience is **not inferred from the service endpoint**.

Explicit values are normalized by trimming surrounding whitespace and trailing `/` characters,
removing one case-insensitive `/.default` suffix, and trimming trailing `/` characters again.
The SDK requests `<normalized-resource-id>/.default`. For example,
`https://durabletask.azure.us//.DEFAULT//` requests `https://durabletask.azure.us/.default`,
and `api://CustomAudience/resource/.DEFAULT/` requests
`api://CustomAudience/resource/.default`. Custom URI casing is preserved.
Whitespace-only values, `///`, `/.default`, and `/.DEFAULT///` throw an `ArgumentException`
instead of silently selecting a default.

**Government-cloud configuration:** the government/DoD default now matches the audience
registered in that cloud, rather than the previous public-cloud `https://durabletask.io`
audience. Supported deployments use a scheduler and credentials in the same cloud;
this setting does not enable cross-cloud scheduler access. Public-cloud deployments retain
their existing default. An explicit `ResourceId = "https://durabletask.io"` on the client
and worker (or in their connection strings) still selects the public audience regardless
of `REGION_NAME`.

#### Configure the credential authority separately

Neither `ResourceId` nor `REGION_NAME` changes the endpoint or the credential's authority.
When supplying a `TokenCredential`, configure the authority on that credential. For example,
the following standalone client and worker configuration explicitly selects Azure Government:

```csharp
using Azure.Identity;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.AzureManaged;
using Microsoft.DurableTask.Worker;
using Microsoft.DurableTask.Worker.AzureManaged;
using Microsoft.Extensions.DependencyInjection;

string endpoint = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_ENDPOINT")
?? throw new InvalidOperationException("DURABLE_TASK_SCHEDULER_ENDPOINT is not set.");
string taskHub = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_TASK_HUB")
?? throw new InvalidOperationException("DURABLE_TASK_SCHEDULER_TASK_HUB is not set.");

DefaultAzureCredential credential = new(new DefaultAzureCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzureGovernment,
});

ServiceCollection services = new();
services.AddDurableTaskClient(builder =>
builder.UseDurableTaskScheduler(endpoint, taskHub, credential,
options => options.ResourceId = "https://durabletask.azure.us"));
services.AddDurableTaskWorker(builder =>
builder.UseDurableTaskScheduler(endpoint, taskHub, credential,
options => options.ResourceId = "https://durabletask.azure.us"));
// Register your orchestrations and activities on the worker before starting the host.
```

For SDK-created credentials, the connection string accepts an independent `AuthorityHost`:

```text
Endpoint=https://<government-scheduler-endpoint>;TaskHub=<task-hub>;Authentication=DefaultAzure;ResourceId=https://durabletask.azure.us;AuthorityHost=https://login.microsoftonline.us/
```

`AuthorityHost` must be an absolute HTTPS URI. It is forwarded for `DefaultAzure`,
`WorkloadIdentity`, `Environment`, `VisualStudio`, `VisualStudioCode`, and `InteractiveBrowser`.
Omitting it (or leaving it empty) preserves Azure Identity defaults, including
`AZURE_AUTHORITY_HOST` where applicable. It is not applied to `ManagedIdentity`, `AzureCLI`,
`AzurePowerShell`, or `None`. Managed identity uses the hosting environment's identity
endpoint. Developer-tool credentials, including those in `DefaultAzureCredential`, may
require separate tool cloud configuration (for example, `az cloud set --name AzureUSGovernment`
before signing in with Azure CLI).

On-demand sandbox management reuses the configured client channel and audience. Sandbox
workers and their registration/reconnect streams share the worker channel and audience;
`UseSandboxWorker()` resolves the same region default. To override it, configure the
corresponding `DurableTaskSchedulerWorkerOptions` using the options system:

```csharp
services.Configure<Microsoft.DurableTask.DurableTaskSchedulerWorkerOptions>(
options => options.ResourceId = "https://durabletask.azure.us");
```

For a named worker, pass its name to `Configure`. Sandbox workers create a managed identity
credential, so an Entra authority override does not apply. Caller-supplied gRPC channels or
call invokers remain responsible for their own authentication.

## Obtaining the Protobuf definitions

This project utilizes protobuf definitions from [durabletask-protobuf](https://github.com/microsoft/durabletask-protobuf), which are copied (vendored) into this repository under the `src/Grpc` directory. See the corresponding [README.md](./src/Grpc/README.md) for more information about how to update the protobuf definitions.
Expand Down
10 changes: 10 additions & 0 deletions samples/on-demand-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ $env:DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID = "<scheduler UMI client ID>"

For `Authentication=DefaultAzure`, sign in with Azure CLI or configure another supported Azure identity before running the main app.

For government-cloud deployments, configure the token audience independently of the endpoint
and credential authority. The main app connection string accepts
`ResourceId=https://durabletask.azure.us;AuthorityHost=https://login.microsoftonline.us/`.
The remote worker uses managed identity (its hosting environment's identity endpoint, not an
Entra authority override). Its audience defaults to `https://durabletask.azure.us` when
`REGION_NAME` starts with `usgov` or `usdod`, case-insensitively. An explicit
`DurableTaskSchedulerWorkerOptions.ResourceId` overrides that default and is shared by work-item
and registration transports. See [token audiences and Azure Government](../../README.md#token-audiences-and-azure-government)
for configuration examples and the government-region default migration.

The worker profile class declares the image, CPU, memory, max concurrency, and on-demand sandbox activity identities with `options.AddActivity(name, version)`. The main app and remote worker both use the `shared/SandboxActivities.cs` constants so the workerProfile and worker registration stay in sync.

You can also set the scheduler connection string in `main-app/appsettings.json`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static class SandboxActivitiesClientServiceCollectionExtensions
/// <summary>
/// Adds a DTS on-demand sandbox activity management client using the default Durable Task client configuration.
/// </summary>
/// <remarks>Reuses the client channel, including its credential and configured token audience.</remarks>
/// <param name="services">The service collection to configure.</param>
/// <returns>The original service collection, for call chaining.</returns>
public static IServiceCollection AddDurableTaskSchedulerSandboxActivitiesClient(this IServiceCollection services)
Expand All @@ -27,6 +28,7 @@ public static IServiceCollection AddDurableTaskSchedulerSandboxActivitiesClient(
/// <summary>
/// Adds a DTS on-demand sandbox activity management client using a named Durable Task client configuration.
/// </summary>
/// <remarks>Reuses the named client channel, including its credential and configured token audience.</remarks>
/// <param name="services">The service collection to configure.</param>
/// <param name="clientName">The Durable Task client name whose scheduler channel should be reused.</param>
/// <returns>The original service collection, for call chaining.</returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public static void UseDurableTaskScheduler(
options.EndpointAddress = connectionOptions.EndpointAddress;
options.TaskHubName = connectionOptions.TaskHubName;
options.Credential = connectionOptions.Credential;
options.CopyResourceIdFrom(connectionOptions);
options.AllowInsecureCredentials = connectionOptions.AllowInsecureCredentials;
},
configure);
Expand Down
49 changes: 40 additions & 9 deletions src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using Azure.Core;
using Azure.Identity;
using Grpc.Core;
Expand All @@ -16,6 +17,9 @@ namespace Microsoft.DurableTask;
/// </summary>
public class DurableTaskSchedulerClientOptions
{
readonly string defaultResourceId = DurableTaskSchedulerResourceId.GetDefault();
string? resourceId;

/// <summary>
/// Gets or sets the endpoint address of the Durable Task Scheduler resource.
/// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io".
Expand All @@ -32,13 +36,27 @@ public class DurableTaskSchedulerClientOptions
/// <summary>
/// Gets or sets the credential used to authenticate with the Durable Task Scheduler task hub resource.
/// </summary>
/// <remarks>Configure the authority host on this credential, separately from <see cref="ResourceId"/>.</remarks>
public TokenCredential? Credential { get; set; }

/// <summary>
/// Gets or sets the resource ID of the Durable Task Scheduler resource.
/// The default value is https://durabletask.io.
/// Gets or sets the token audience URI, not an Azure Resource Manager resource path.
/// </summary>
public string ResourceId { get; set; } = "https://durabletask.io";
/// <remarks>
/// Null or empty values use the default resolved when these options are created:
/// <c>https://durabletask.azure.us</c> when <c>REGION_NAME</c> starts with <c>usgov</c> or
/// <c>usdod</c> (case-insensitive), or <c>https://durabletask.io</c> otherwise.
/// Explicit values have surrounding whitespace, trailing slashes, and one existing
/// <c>/.default</c> suffix removed. Token requests append <c>/.default</c> to the result.
/// This does not change <see cref="EndpointAddress"/> or the credential's authority host.
/// </remarks>
/// <exception cref="ArgumentException">The explicit value is empty after normalization.</exception>
[AllowNull]
public string ResourceId
{
get => this.resourceId ?? this.defaultResourceId;
set => this.resourceId = DurableTaskSchedulerResourceId.Normalize(value);
}

/// <summary>
/// Gets or sets a value indicating whether to allow insecure channel credentials.
Expand All @@ -55,6 +73,12 @@ public class DurableTaskSchedulerClientOptions
/// Creates a new instance of <see cref="DurableTaskSchedulerClientOptions"/> from a connection string.
/// </summary>
/// <param name="connectionString">The connection string to parse.</param>
/// <remarks>
/// Supports an optional <c>ResourceId</c> token audience and an independent <c>AuthorityHost</c>
/// HTTPS URI for Azure Identity credentials that support authority configuration. Omitting
/// <c>AuthorityHost</c> preserves Azure Identity defaults, including <c>AZURE_AUTHORITY_HOST</c>.
/// Managed identity uses its hosting environment; developer tools may need separate cloud configuration.
/// </remarks>
/// <returns>A new instance of <see cref="DurableTaskSchedulerClientOptions"/>.</returns>
public static DurableTaskSchedulerClientOptions FromConnectionString(string connectionString)
{
Expand All @@ -75,10 +99,17 @@ internal static DurableTaskSchedulerClientOptions FromConnectionString(
EndpointAddress = connectionString.Endpoint,
TaskHubName = connectionString.TaskHubName,
Credential = credential,
ResourceId = connectionString.ResourceId,
AllowInsecureCredentials = credential is null,
};
}

/// <summary>
/// Copies an already normalized audience without stripping a second meaningful <c>/.default</c> segment.
/// </summary>
/// <param name="source">The options with the resolved audience.</param>
internal void CopyResourceIdFrom(DurableTaskSchedulerClientOptions source) => this.resourceId = source.ResourceId;

/// <summary>
/// Creates a gRPC channel for communicating with the Durable Task Scheduler service.
/// </summary>
Expand Down Expand Up @@ -173,11 +204,11 @@ this.Credential is not null
switch (authType.ToLowerInvariant())
{
case "defaultazure":
return new DefaultAzureCredential(); // CodeQL [SM05137] Use DefaultAzureCredential explicitly for local development and is decided by the user
return new DefaultAzureCredential(connectionString.CreateCredentialOptions<DefaultAzureCredentialOptions>()); // CodeQL [SM05137] Use DefaultAzureCredential explicitly for local development and is decided by the user
case "managedidentity":
return new ManagedIdentityCredential(connectionString.ClientId);
case "workloadidentity":
WorkloadIdentityCredentialOptions opts = new WorkloadIdentityCredentialOptions();
WorkloadIdentityCredentialOptions opts = connectionString.CreateCredentialOptions<WorkloadIdentityCredentialOptions>();
if (!string.IsNullOrEmpty(connectionString.ClientId))
{
opts.ClientId = connectionString.ClientId;
Expand All @@ -198,17 +229,17 @@ this.Credential is not null

return new WorkloadIdentityCredential(opts);
case "environment":
return new EnvironmentCredential();
return new EnvironmentCredential(connectionString.CreateCredentialOptions<EnvironmentCredentialOptions>());
case "azurecli":
return new AzureCliCredential();
case "azurepowershell":
return new AzurePowerShellCredential();
case "visualstudio":
return new VisualStudioCredential();
return new VisualStudioCredential(connectionString.CreateCredentialOptions<VisualStudioCredentialOptions>());
case "visualstudiocode":
return new VisualStudioCodeCredential();
return new VisualStudioCodeCredential(connectionString.CreateCredentialOptions<VisualStudioCodeCredentialOptions>());
case "interactivebrowser":
return new InteractiveBrowserCredential();
return new InteractiveBrowserCredential(connectionString.CreateCredentialOptions<InteractiveBrowserCredentialOptions>());
case "none":
return null;
default:
Expand Down
2 changes: 2 additions & 0 deletions src/Client/AzureManaged/RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
- Support normalized ResourceId token audiences in options and connection strings, with per-instance government/DoD defaults that select the audience registered in that cloud. Public-cloud defaults are unchanged; cross-cloud scheduler access is unsupported.
- Support independent AuthorityHost connection-string configuration for SDK-created credentials that accept an authority, preserving Azure Identity defaults when omitted.
- Released first version Microsoft.DurableTask.Client.AzureManaged - 1.5.0-preview.1
32 changes: 32 additions & 0 deletions src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Data.Common;
using Azure.Identity;

namespace Microsoft.DurableTask;

Expand Down Expand Up @@ -59,8 +60,39 @@ public DurableTaskSchedulerConnectionString(string connectionString)
/// </summary>
public string TaskHubName => this.GetRequiredValue("TaskHub");

/// <summary>
/// Gets the optional token audience URI. Normalization is performed by the scheduler options.
/// </summary>
public string? ResourceId => this.GetValue("ResourceId");

string? AdditionallyAllowedTenantsStr => this.GetValue("AdditionallyAllowedTenants");

/// <summary>
/// Creates credential options, forwarding an explicit authority only when supplied.
/// </summary>
/// <typeparam name="TOptions">The Azure Identity options type.</typeparam>
/// <returns>Options for a credential that supports authority configuration.</returns>
public TOptions CreateCredentialOptions<TOptions>()
where TOptions : TokenCredentialOptions, new()
{
TOptions options = new();
string? authorityHost = this.GetValue("AuthorityHost");
if (!string.IsNullOrEmpty(authorityHost))
{
if (!Uri.TryCreate(authorityHost, UriKind.Absolute, out Uri? authority)
|| authority.Scheme != Uri.UriSchemeHttps)
{
throw new ArgumentException(
"The connection string AuthorityHost must be an absolute HTTPS URI, such as https://login.microsoftonline.us/.",
"connectionString");
}

options.AuthorityHost = authority;
}

return options;
}

string? GetValue(string name) =>
this.builder.TryGetValue(name, out object? value)
? value as string
Expand Down
Loading
Loading