Skip to content

Commit 3ee0724

Browse files
ReubenBondCopilot
andcommitted
fix(docs): localize authenticated connection snippets
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 520be5e commit 3ee0724

7 files changed

Lines changed: 379 additions & 9 deletions

File tree

docs/site/src/content/docs/host/authenticated-silo-connections.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Prefer an explicit <xref:Azure.Core.TokenCredential> appropriate to the hosting
164164
environment. The maintained sample supplies a `WorkloadIdentityCredential`; it
165165
doesn't silently use a developer or unrelated cached identity:
166166

167-
:::code language="csharp" source="../../../../../../samples/AuthenticatedSiloConnections/Program.cs" id="ExplicitCredential":::
167+
:::code language="csharp" source="snippets/authenticated-silo-connections/csharp/Program.cs" id="ExplicitCredential":::
168168

169169
Create the credential once and reuse it. The credential implementation owns its
170170
token cache.
@@ -178,7 +178,7 @@ public or private roots in the platform trust store and overlap old and new
178178
roots there during CA rotation. Each silo certificate therefore needs both the
179179
Server Authentication and Client Authentication EKUs.
180180

181-
:::code language="csharp" source="../../../../../../samples/AuthenticatedSiloConnections/SiloAuthentication.cs" id="AuthenticatedSiloConnections":::
181+
:::code language="csharp" source="snippets/authenticated-silo-connections/csharp/SiloAuthentication.cs" id="AuthenticatedSiloConnections":::
182182

183183
The configured `TargetHost` must match a DNS SAN and the chain must be valid
184184
and trusted. Never replace this policy with
@@ -203,11 +203,11 @@ token for outbound silo connections, so it needs both a provider and validator.
203203
Configure the gateway side on every silo. It validates client tokens before the
204204
gateway reads the Orleans connection preamble:
205205

206-
:::code language="csharp" source="../../../../../../samples/AuthenticatedSiloConnections/SiloAuthentication.cs" id="AuthenticatedClientGateway":::
206+
:::code language="csharp" source="snippets/authenticated-silo-connections/csharp/SiloAuthentication.cs" id="AuthenticatedClientGateway":::
207207

208208
Configure each external Orleans client with the corresponding outbound policy:
209209

210-
:::code language="csharp" source="../../../../../../samples/AuthenticatedSiloConnections/ClientAuthentication.cs" id="AuthenticatedClient":::
210+
:::code language="csharp" source="snippets/authenticated-silo-connections/csharp/ClientAuthentication.cs" id="AuthenticatedClient":::
211211

212212
The client and gateway must use compatible enforcement modes and the same Entra
213213
audience, tenant, cluster binding, client role, and caller authorization. Keep
@@ -330,7 +330,7 @@ hide a broken credential until token-expiry recycling or a process restart.
330330
Export the `Microsoft.Orleans.Connections.Security` meter. The maintained
331331
sample enables an OTLP exporter when `OTEL_EXPORTER_OTLP_ENDPOINT` is set:
332332

333-
:::code language="csharp" source="../../../../../../samples/AuthenticatedSiloConnections/Program.cs" id="FixedDiagnostics":::
333+
:::code language="csharp" source="snippets/authenticated-silo-connections/csharp/Program.cs" id="FixedDiagnostics":::
334334

335335
Alert on rates and latency for these instruments:
336336

docs/site/src/content/docs/host/snippets/authenticated-silo-connections/csharp/AuthenticatedSiloConnections.Snippets.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<Compile Include="$(RepositoryRoot)samples\AuthenticatedSiloConnections\Program.cs" Link="Program.cs" />
13-
<Compile Include="$(RepositoryRoot)samples\AuthenticatedSiloConnections\ClientAuthentication.cs" Link="ClientAuthentication.cs" />
14-
<Compile Include="$(RepositoryRoot)samples\AuthenticatedSiloConnections\SampleOptions.cs" Link="SampleOptions.cs" />
15-
<Compile Include="$(RepositoryRoot)samples\AuthenticatedSiloConnections\SiloAuthentication.cs" Link="SiloAuthentication.cs" />
12+
<Compile Include="Program.cs" />
13+
<Compile Include="ClientAuthentication.cs" />
14+
<Compile Include="SampleOptions.cs" />
15+
<Compile Include="SiloAuthentication.cs" />
1616
</ItemGroup>
1717

1818
<ItemGroup>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Azure.Core;
2+
using Orleans.Hosting;
3+
4+
namespace AuthenticatedSiloConnections;
5+
6+
internal static class ClientAuthentication
7+
{
8+
public static void Configure(
9+
IClientBuilder clientBuilder,
10+
SampleOptions options,
11+
TokenCredential credential)
12+
{
13+
// <AuthenticatedClient>
14+
clientBuilder.UseAuthenticatedClientConnections(
15+
tls =>
16+
{
17+
tls.CheckCertificateRevocation = true;
18+
},
19+
authentication =>
20+
{
21+
SiloAuthentication.ConfigureAuthentication(
22+
authentication,
23+
options,
24+
credential,
25+
options.Entra.AllowedClientCallerClientIds,
26+
"Orleans.Client.Connect");
27+
});
28+
// </AuthenticatedClient>
29+
}
30+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using AuthenticatedSiloConnections;
2+
using Azure.Core;
3+
using Azure.Identity;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Logging;
7+
using OpenTelemetry.Metrics;
8+
using OpenTelemetry.Resources;
9+
using Orleans.Hosting;
10+
11+
var builder = Host.CreateApplicationBuilder(args);
12+
var options = SampleOptions.Load(builder.Configuration);
13+
var exportToOtlp = !string.IsNullOrWhiteSpace(
14+
builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
15+
16+
// <FixedDiagnostics>
17+
builder.Logging.ClearProviders();
18+
builder.Logging.AddJsonConsole(console =>
19+
{
20+
console.TimestampFormat = "O";
21+
console.JsonWriterOptions = new() { Indented = false };
22+
});
23+
builder.Logging.AddFilter("Orleans.Connections.Security", LogLevel.Information);
24+
builder.Logging.AddFilter("Azure.Identity", LogLevel.Warning);
25+
26+
builder.Services.AddOpenTelemetry()
27+
.ConfigureResource(resource => resource.AddService(
28+
serviceName: "authenticated-orleans-silo",
29+
serviceInstanceId: Environment.MachineName))
30+
.WithMetrics(metrics =>
31+
{
32+
metrics.AddMeter("Microsoft.Orleans.Connections.Security");
33+
34+
if (exportToOtlp)
35+
{
36+
metrics.AddOtlpExporter();
37+
}
38+
});
39+
// </FixedDiagnostics>
40+
41+
// <ExplicitCredential>
42+
TokenCredential credential = new WorkloadIdentityCredential(
43+
new WorkloadIdentityCredentialOptions
44+
{
45+
TenantId = options.Entra.TenantId,
46+
ClientId = options.Entra.WorkloadClientId,
47+
TokenFilePath = options.Entra.FederatedTokenFile,
48+
});
49+
// </ExplicitCredential>
50+
51+
using var siloCertificate = CertificatePolicy.LoadSiloCertificate(
52+
options.Certificate.Path,
53+
options.Certificate.Password);
54+
55+
builder.UseOrleans(siloBuilder =>
56+
{
57+
siloBuilder.UseLocalhostClustering(
58+
siloPort: options.SiloPort,
59+
gatewayPort: options.GatewayPort,
60+
primarySiloEndpoint: options.PrimarySiloEndpoint,
61+
serviceId: options.ServiceId,
62+
clusterId: options.ClusterId);
63+
64+
SiloAuthentication.Configure(
65+
siloBuilder,
66+
options,
67+
credential,
68+
siloCertificate);
69+
});
70+
71+
await builder.Build().RunAsync();
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System.Net;
2+
using Microsoft.Extensions.Configuration;
3+
using Orleans.Connections.Security;
4+
5+
namespace AuthenticatedSiloConnections;
6+
7+
internal sealed class SampleOptions
8+
{
9+
public const string SectionName = "OrleansSecurity";
10+
11+
public string ServiceId { get; set; } = "authenticated-silo-sample";
12+
13+
public string ClusterId { get; set; } = "";
14+
15+
public int SiloPort { get; set; } = 11111;
16+
17+
public int GatewayPort { get; set; } = 30000;
18+
19+
public int PrimarySiloPort { get; set; } = 11111;
20+
21+
public SiloConnectionAuthenticationMode AuthenticationMode { get; set; }
22+
= SiloConnectionAuthenticationMode.Audit;
23+
24+
public CertificateOptions Certificate { get; set; } = new();
25+
26+
public EntraOptions Entra { get; set; } = new();
27+
28+
public IPEndPoint PrimarySiloEndpoint
29+
=> new(IPAddress.Loopback, PrimarySiloPort);
30+
31+
public static SampleOptions Load(IConfiguration configuration)
32+
{
33+
var result = configuration
34+
.GetRequiredSection(SectionName)
35+
.Get<SampleOptions>()
36+
?? throw new InvalidOperationException(
37+
$"Configuration section '{SectionName}' is required.");
38+
39+
result.Validate();
40+
return result;
41+
}
42+
43+
private void Validate()
44+
{
45+
RequireValue(ServiceId, nameof(ServiceId));
46+
RequireValue(ClusterId, nameof(ClusterId));
47+
ValidatePort(SiloPort, nameof(SiloPort));
48+
ValidatePort(GatewayPort, nameof(GatewayPort));
49+
ValidatePort(PrimarySiloPort, nameof(PrimarySiloPort));
50+
Certificate.Validate();
51+
Entra.Validate(ClusterId);
52+
}
53+
54+
private static void ValidatePort(int value, string name)
55+
{
56+
if (value is < IPEndPoint.MinPort or > IPEndPoint.MaxPort)
57+
{
58+
throw new InvalidOperationException($"{name} is outside the valid port range.");
59+
}
60+
}
61+
62+
internal static void RequireValue(string? value, string name)
63+
{
64+
if (string.IsNullOrWhiteSpace(value)
65+
|| value.Contains('<')
66+
|| value.Contains('>'))
67+
{
68+
throw new InvalidOperationException(
69+
$"{SectionName}:{name} must be explicitly configured.");
70+
}
71+
}
72+
}
73+
74+
internal sealed class CertificateOptions
75+
{
76+
public string Path { get; set; } = "";
77+
78+
public string? Password { get; set; }
79+
80+
public string TargetHost { get; set; } = "";
81+
82+
public void Validate()
83+
{
84+
SampleOptions.RequireValue(Path, "Certificate:Path");
85+
SampleOptions.RequireValue(TargetHost, "Certificate:TargetHost");
86+
87+
if (!File.Exists(Path))
88+
{
89+
throw new InvalidOperationException(
90+
"The configured silo certificate file does not exist.");
91+
}
92+
}
93+
}
94+
95+
internal sealed class EntraOptions
96+
{
97+
public string TenantId { get; set; } = "";
98+
99+
public string ResourceApplicationId { get; set; } = "";
100+
101+
public string WorkloadClientId { get; set; } = "";
102+
103+
public string FederatedTokenFile { get; set; } = "";
104+
105+
public string[] AllowedSiloCallerClientIds { get; set; } = [];
106+
107+
public string[] AllowedClientCallerClientIds { get; set; } = [];
108+
109+
public Uri Authority
110+
=> new($"https://login.microsoftonline.com/{TenantId}/v2.0");
111+
112+
public string Audience
113+
=> $"api://{ResourceApplicationId}/{_clusterId}";
114+
115+
private string _clusterId = "";
116+
117+
public void Validate(string clusterId)
118+
{
119+
_clusterId = clusterId;
120+
RequireGuid(TenantId, nameof(TenantId));
121+
RequireGuid(ResourceApplicationId, nameof(ResourceApplicationId));
122+
RequireGuid(WorkloadClientId, nameof(WorkloadClientId));
123+
SampleOptions.RequireValue(FederatedTokenFile, $"Entra:{nameof(FederatedTokenFile)}");
124+
125+
if (!File.Exists(FederatedTokenFile))
126+
{
127+
throw new InvalidOperationException(
128+
"The configured workload identity token file does not exist.");
129+
}
130+
131+
if (AllowedSiloCallerClientIds.Length == 0)
132+
{
133+
throw new InvalidOperationException(
134+
"At least one allowed silo caller application ID is required.");
135+
}
136+
137+
if (AllowedClientCallerClientIds.Length == 0)
138+
{
139+
throw new InvalidOperationException(
140+
"At least one allowed external client application ID is required.");
141+
}
142+
143+
foreach (var clientId in AllowedSiloCallerClientIds.Concat(AllowedClientCallerClientIds))
144+
{
145+
RequireGuid(clientId, "AllowedCallerClientIds");
146+
}
147+
148+
if (!AllowedSiloCallerClientIds
149+
.Concat(AllowedClientCallerClientIds)
150+
.Contains(WorkloadClientId, StringComparer.OrdinalIgnoreCase))
151+
{
152+
throw new InvalidOperationException(
153+
"This process's workload client ID must be in an allowed caller list.");
154+
}
155+
}
156+
157+
private static void RequireGuid(string value, string name)
158+
{
159+
if (!Guid.TryParseExact(value, "D", out _))
160+
{
161+
throw new InvalidOperationException(
162+
$"{SampleOptions.SectionName}:Entra:{name} must be a GUID.");
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)