Skip to content

Commit 8e68a8d

Browse files
committed
Refactor SQL Server setup and connection handling with TLS support
- Updated SQL Server resource to configure a TLS cert when one is configured, and remove the TLS validation bypass from the connection string - Updated Playground app host to include sql with no TLS, and renamed resources to make their purpose clearer
1 parent 2ffbcff commit 8e68a8d

File tree

4 files changed

+92
-30
lines changed

4 files changed

+92
-30
lines changed

playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs renamed to playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/AppHost.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
5+
46
var builder = DistributedApplication.CreateBuilder(args);
57

6-
var sql1 = builder.AddAzureSqlServer("sql1")
8+
var sqlLocal = builder.AddAzureSqlServer("sqlLocal")
79
.RunAsContainer();
810

9-
var db1 = sql1.AddDatabase("db1");
11+
var dbLocal = sqlLocal.AddDatabase("dbLocal");
12+
13+
var sqlAzure = builder.AddAzureSqlServer("sqlAzure");
14+
var dbAzure = sqlAzure.AddDatabase("dbAzure");
1015

11-
var sql2 = builder.AddAzureSqlServer("sql2");
12-
var db2 = sql2.AddDatabase("db2");
16+
var sqlNoTls = builder.AddSqlServer("sqlNoTls")
17+
.WithoutHttpsCertificate();
1318

1419
var dbsetup = builder.AddProject<Projects.SqlServerEndToEnd_DbSetup>("dbsetup")
15-
.WithReference(db1).WaitFor(sql1)
16-
.WithReference(db2).WaitFor(sql2);
20+
.WithReference(dbLocal).WaitFor(sqlLocal)
21+
.WithReference(dbAzure).WaitFor(sqlAzure);
1722

1823
builder.AddProject<Projects.SqlServerEndToEnd_ApiService>("api")
1924
.WithExternalHttpEndpoints()
20-
.WithReference(db1).WaitFor(db1)
21-
.WithReference(db2).WaitFor(db2)
25+
.WithReference(dbLocal).WaitFor(dbLocal)
26+
.WithReference(dbAzure).WaitFor(dbAzure)
2227
.WaitForCompletion(dbsetup);
2328

2429
#if !SKIP_DASHBOARD_REFERENCE

src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
14+
<Compile Include="$(SharedDir)X509Certificate2Extensions.cs" Link="Utils\X509Certificate2Extensions.cs" />
1415
</ItemGroup>
1516

1617
<ItemGroup>

src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
using Microsoft.Data.SqlClient;
1010
using Microsoft.Extensions.DependencyInjection;
1111
using Microsoft.Extensions.Logging;
12+
using Aspire.Hosting.Utils;
1213

1314
namespace Aspire.Hosting;
1415

16+
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
17+
1518
/// <summary>
1619
/// Provides extension methods for adding SQL Server resources to the application model.
1720
/// </summary>
@@ -49,7 +52,7 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr
4952
var healthCheckKey = $"{name}_check";
5053
builder.Services.AddHealthChecks().AddSqlServer(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);
5154

52-
return builder.AddResource(sqlServer)
55+
var sqlBuilder = builder.AddResource(sqlServer)
5356
.WithEndpoint(port: port, targetPort: 1433, name: SqlServerServerResource.PrimaryEndpointName)
5457
.WithImage(SqlServerContainerImageTags.Image, SqlServerContainerImageTags.Tag)
5558
.WithImageRegistry(SqlServerContainerImageTags.Registry)
@@ -89,6 +92,55 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr
8992
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
9093
}
9194
});
95+
96+
return sqlBuilder
97+
.SubscribeHttpsEndpointsUpdate(ctx =>
98+
{
99+
var executionContext = ctx.Services.GetRequiredService<DistributedApplicationExecutionContext>();
100+
101+
// Dev cert versions prior to 6 don't include "127.0.0.1" int eh SAN, and so won't be trusted
102+
// So only enable TLS if we have a custom cert, or a dev cert with version 6 or higher.
103+
if (executionContext.IsRunMode)
104+
{
105+
ctx.Resource.TryGetLastAnnotation<HttpsCertificateAnnotation>(out var certAnnotation);
106+
107+
if (certAnnotation == null || certAnnotation.UseDeveloperCertificate == true)
108+
{
109+
var devCertService = ctx.Services.GetRequiredService<IDeveloperCertificateService>();
110+
var cert = devCertService.Certificates.First();
111+
if (cert.GetCertificateVersion() < 6)
112+
{
113+
return;
114+
}
115+
}
116+
}
117+
118+
sqlBuilder.WithEndpoint(SqlServerServerResource.PrimaryEndpointName, x => x.TlsEnabled = true);
119+
})
120+
.WithContainerFiles("/var/opt/mssql/", async (ctx, ct) =>
121+
{
122+
var certContext = ctx.HttpsCertificateContext;
123+
124+
if (certContext is null)
125+
{
126+
return [];
127+
}
128+
129+
var config = $"""
130+
[network]
131+
tlscert = {await certContext.CertificatePath.GetValueAsync(ct).ConfigureAwait(false)}
132+
tlskey = {await certContext.KeyPath.GetValueAsync(ct).ConfigureAwait(false)}
133+
forceencryption = 1
134+
""";
135+
136+
return [
137+
new ContainerFile
138+
{
139+
Name = "mssql.conf",
140+
Contents = config
141+
}
142+
];
143+
});
92144
}
93145

94146
/// <summary>

src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,19 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n
4545
/// </summary>
4646
public ParameterResource PasswordParameter { get; private set; }
4747

48-
private ReferenceExpression ConnectionString =>
49-
ReferenceExpression.Create(
50-
$"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};User ID={DefaultUserName};Password={PasswordParameter};TrustServerCertificate=true");
48+
private ReferenceExpression BuildConnectionString()
49+
{
50+
var builder = new ReferenceExpressionBuilder();
51+
52+
builder.Append($"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};");
53+
builder.Append($"User ID={UserNameReference};");
54+
builder.Append($"Password={PasswordParameter};");
55+
builder.Append($"{PrimaryEndpoint.GetTlsValue(
56+
enabledValue: ReferenceExpression.Empty,
57+
disabledValue: ReferenceExpression.Create($"TrustServerCertificate=true;"))}");
58+
59+
return builder.Build();
60+
}
5161

5262
/// <summary>
5363
/// Gets a reference to the user name for the SQL Server.
@@ -69,19 +79,17 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n
6979
internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = null)
7080
{
7181
var builder = new ReferenceExpressionBuilder();
72-
builder.AppendLiteral("jdbc:sqlserver://");
73-
builder.Append($"{Host}");
74-
builder.AppendLiteral(":");
75-
builder.Append($"{Port}");
82+
83+
builder.Append($"jdbc:sqlserver://{Host}:{Port}");
7684

7785
if (!string.IsNullOrEmpty(databaseName))
7886
{
79-
var databaseNameReference = ReferenceExpression.Create($"{databaseName:uri}");
80-
builder.AppendLiteral(";databaseName=");
81-
builder.Append($"{databaseNameReference}");
87+
builder.Append($"databaseName={databaseName:uri}");
8288
}
8389

84-
builder.AppendLiteral(";trustServerCertificate=true");
90+
builder.Append($"{PrimaryEndpoint.GetTlsValue(
91+
enabledValue: ReferenceExpression.Empty,
92+
disabledValue: ReferenceExpression.Create($"trustServerCertificate=true;"))}");
8593

8694
return builder.Build();
8795
}
@@ -90,8 +98,9 @@ internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = nu
9098
/// Gets the JDBC connection string for the SQL Server.
9199
/// </summary>
92100
/// <remarks>
93-
/// <para>Format: <c>jdbc:sqlserver://{host}:{port};trustServerCertificate=true</c>.</para>
94-
/// <para>User and password credentials are not included in the JDBC connection string. Use the <c>Username</c> and <c>Password</c> connection properties to access credentials.</para>
101+
/// <para>Format: <c>jdbc:sqlserver://{host}:{port}[];trustServerCertificate=true]</c>.</para>
102+
/// <para>User and password credentials are not included in the JDBC connection string.
103+
/// Use the <see cref="UserNameReference"/> and <see cref="PasswordParameter"/> connection properties to access credentials.</para>
95104
/// </remarks>
96105
public ReferenceExpression JdbcConnectionString => BuildJdbcConnectionString();
97106

@@ -107,23 +116,18 @@ public ReferenceExpression ConnectionStringExpression
107116
return connectionStringAnnotation.Resource.ConnectionStringExpression;
108117
}
109118

110-
return ConnectionString;
119+
return BuildConnectionString();
111120
}
112121
}
113122

114123
/// <summary>
115124
/// Gets the connection string for the SQL Server.
116125
/// </summary>
117126
/// <param name="cancellationToken"> A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
118-
/// <returns>A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true".</returns>
127+
/// <returns>A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password", with "TrustServerCertificate=true" appended when TLS certificate material is not configured.</returns>
119128
public ValueTask<string?> GetConnectionStringAsync(CancellationToken cancellationToken = default)
120129
{
121-
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
122-
{
123-
return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken);
124-
}
125-
126-
return ConnectionString.GetValueAsync(cancellationToken);
130+
return ConnectionStringExpression.GetValueAsync(cancellationToken);
127131
}
128132

129133
private readonly Dictionary<string, string> _databases = new(StringComparers.ResourceName);

0 commit comments

Comments
 (0)