Skip to content

Commit 8569041

Browse files
Mike Blairclaude
andcommitted
fix: release readiness — SQL params, tenant validation, token store, ETL cache
- Fix double @@ SQL parameter prefix in MsSqlDataCommandTranslatorBase - Separate tenant access denied (403) from query failure (500) in TenantResolutionMiddleware - Add TenantAccessCheckFailed log method (EventId 567) - Fix EventId collision 7542→7550 in MultitenancyDisabledCode - Add no-op Operations initialize block in ServiceTypeExtensions - Migrate MsSqlTokenStoreLog from [LoggerMessage] to [MessageLogging] - Refactor MsSqlTokenStore to use IConnectionProvider instead of raw connection strings - Replace Guid.Parse with TryParse and AddWithValue with typed SqlParameters - Add LookupTransformType.ClearCache() to StreamingPipeline finally block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a299a7d commit 8569041

10 files changed

Lines changed: 191 additions & 62 deletions

File tree

src/FractalDataWorks.Data.MsSql/Translators/MsSqlDataCommandTranslatorBase.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ private static string BuildWhereCondition(FilterCondition condition, SqlBuildCon
163163
foreach (var item in enumerable)
164164
{
165165
var paramName = $"{context.ParameterPrefix}p{conditionIndex}_{itemIndex++}";
166-
paramNames.Add($"@{paramName}");
167-
var parameter = new SqlParameter($"@{paramName}", item ?? DBNull.Value);
166+
paramNames.Add(paramName);
167+
var parameter = new SqlParameter(paramName, item ?? DBNull.Value);
168168
context.Command.Parameters.Add(parameter);
169169
}
170170

@@ -178,15 +178,15 @@ private static string BuildWhereCondition(FilterCondition condition, SqlBuildCon
178178
}
179179

180180
var singleParamName = $"{context.ParameterPrefix}p{context.ParameterCounter++}";
181-
var sql = $"{columnName} {condition.Operator.SqlOperator} @{singleParamName}";
181+
var sql = $"{columnName} {condition.Operator.SqlOperator} {singleParamName}";
182182

183183
// ZERO SQL INJECTION: Value goes into parameter, NOT concatenated into SQL
184184
// Preprocess string values to escape operator-specific metacharacters (e.g., LIKE wildcards)
185185
object? paramValue = condition.Value is string strValue
186186
? condition.Operator.PreprocessSqlValue(strValue)
187187
: condition.Value;
188188

189-
var singleParameter = new SqlParameter($"@{singleParamName}", paramValue ?? DBNull.Value);
189+
var singleParameter = new SqlParameter(singleParamName, paramValue ?? DBNull.Value);
190190
context.Command.Parameters.Add(singleParameter);
191191
return sql;
192192
}

src/FractalDataWorks.Hosting/Extensions/ServiceTypeExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ public static WebApplication InitializeFrameworkServiceTypes(
9595
SchedulerTypes.Initialize(app.Services, loggerFactory);
9696
}
9797

98+
if (state.HasOperations)
99+
{
100+
HostingLog.ServiceTypeRegistrationPhase(logger, "Initialize", "Operations");
101+
// Operations services are registered via AddOperations() but require no Initialize phase.
102+
// This block exists for pattern consistency with other service types.
103+
}
104+
98105
if (state.HasConfigurationWriters)
99106
{
100107
app.Services.InitializeConfigurationWriters(loggerFactory);

src/FractalDataWorks.Services.Authentication.Jwt.MsSql/Extensions/MsSqlTokenStoreServiceCollectionExtensions.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using FractalDataWorks.Services.Authentication.Jwt.MsSql.Storage;
22
using FractalDataWorks.Services.Authentication.Jwt.Storage;
3+
using FractalDataWorks.Services.Connections.Abstractions;
34
using Microsoft.Extensions.DependencyInjection;
45
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.Logging;
57

68
namespace FractalDataWorks.Services.Authentication.Jwt.MsSql.Extensions;
79

@@ -15,22 +17,35 @@ public static class MsSqlTokenStoreServiceCollectionExtensions
1517
/// <see cref="MsSqlTokenStore"/> backed by a SQL Server database.
1618
/// </summary>
1719
/// <param name="services">The service collection.</param>
18-
/// <param name="connectionString">The SQL Server connection string for the auth database.</param>
20+
/// <param name="connectionName">
21+
/// The name of the connection to resolve via <see cref="IConnectionProvider"/>.
22+
/// Defaults to <c>"AuthDb"</c>.
23+
/// </param>
1924
/// <returns>The service collection for chaining.</returns>
2025
/// <remarks>
26+
/// <para>
2127
/// Call this after <c>JwtAuthenticationType.RegisterRequiredServices</c> to override
2228
/// the default <see cref="InMemoryTokenStore"/> with the SQL-backed implementation.
29+
/// </para>
30+
/// <para>
31+
/// The connection is resolved at runtime via <see cref="IConnectionProvider"/>,
32+
/// supporting secret manager integration, Entra ID authentication, and
33+
/// configuration reload from the database.
34+
/// </para>
35+
/// <para>
2336
/// The SQL Server database must have the <c>auth.RefreshToken</c> and
2437
/// <c>auth.RevokedAccessToken</c> tables created before calling this.
38+
/// </para>
2539
/// </remarks>
2640
public static IServiceCollection AddMsSqlTokenStore(
2741
this IServiceCollection services,
28-
string connectionString)
42+
string connectionName = "AuthDb")
2943
{
3044
services.Replace(ServiceDescriptor.Singleton<ITokenStore>(sp =>
3145
{
32-
var logger = sp.GetService<Microsoft.Extensions.Logging.ILogger<MsSqlTokenStore>>();
33-
return new MsSqlTokenStore(connectionString, logger);
46+
var connectionProvider = sp.GetRequiredService<IConnectionProvider>();
47+
var logger = sp.GetRequiredService<ILogger<MsSqlTokenStore>>();
48+
return new MsSqlTokenStore(connectionProvider, connectionName, logger);
3449
}));
3550

3651
return services;

src/FractalDataWorks.Services.Authentication.Jwt.MsSql/FractalDataWorks.Services.Authentication.Jwt.MsSql.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,11 @@
2222
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
2323
</ItemGroup>
2424

25+
<ItemGroup>
26+
<ProjectReference Include="..\FractalDataWorks.MessageLogging.Abstractions\FractalDataWorks.MessageLogging.Abstractions.csproj" />
27+
<ProjectReference Include="..\FractalDataWorks.MessageLogging.SourceGenerators\FractalDataWorks.MessageLogging.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
28+
<ProjectReference Include="..\FractalDataWorks.Services.Connections.Abstractions\FractalDataWorks.Services.Connections.Abstractions.csproj" />
29+
<ProjectReference Include="..\FractalDataWorks.Services.Connections.MsSql\FractalDataWorks.Services.Connections.MsSql.csproj" />
30+
</ItemGroup>
31+
2532
</Project>
Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using FractalDataWorks.MessageLogging;
3+
using FractalDataWorks.Messages;
24
using Microsoft.Extensions.Logging;
35

46
namespace FractalDataWorks.Services.Authentication.Jwt.MsSql.Logging;
@@ -10,26 +12,38 @@ namespace FractalDataWorks.Services.Authentication.Jwt.MsSql.Logging;
1012
public static partial class MsSqlTokenStoreLog
1113
{
1214
/// <summary>Logs when storing a refresh token fails.</summary>
13-
[LoggerMessage(EventId = 7481, Level = LogLevel.Error, Message = "Database error storing refresh token for user {UserId}")]
14-
public static partial void StoreRefreshTokenFailed(ILogger logger, Exception exception, string userId);
15+
[MessageLogging(EventId = 7481, Level = LogLevel.Error, Message = "Database error storing refresh token for user {userId}")]
16+
public static partial IGenericMessage StoreRefreshTokenFailed(ILogger logger, Exception exception, string userId);
1517

1618
/// <summary>Logs when validating a refresh token fails.</summary>
17-
[LoggerMessage(EventId = 7482, Level = LogLevel.Error, Message = "Database error validating refresh token for user {UserId}")]
18-
public static partial void ValidateRefreshTokenFailed(ILogger logger, Exception exception, string userId);
19+
[MessageLogging(EventId = 7482, Level = LogLevel.Error, Message = "Database error validating refresh token for user {userId}")]
20+
public static partial IGenericMessage ValidateRefreshTokenFailed(ILogger logger, Exception exception, string userId);
1921

2022
/// <summary>Logs when invalidating user refresh tokens fails.</summary>
21-
[LoggerMessage(EventId = 7483, Level = LogLevel.Error, Message = "Database error invalidating refresh tokens for user {UserId}")]
22-
public static partial void InvalidateRefreshTokensFailed(ILogger logger, Exception exception, string userId);
23+
[MessageLogging(EventId = 7483, Level = LogLevel.Error, Message = "Database error invalidating refresh tokens for user {userId}")]
24+
public static partial IGenericMessage InvalidateRefreshTokensFailed(ILogger logger, Exception exception, string userId);
2325

2426
/// <summary>Logs when revoking an access token fails.</summary>
25-
[LoggerMessage(EventId = 7484, Level = LogLevel.Error, Message = "Database error revoking access token with JTI {Jti}")]
26-
public static partial void RevokeAccessTokenFailed(ILogger logger, Exception exception, string jti);
27+
[MessageLogging(EventId = 7484, Level = LogLevel.Error, Message = "Database error revoking access token with JTI {jti}")]
28+
public static partial IGenericMessage RevokeAccessTokenFailed(ILogger logger, Exception exception, string jti);
2729

2830
/// <summary>Logs when checking if an access token is revoked fails.</summary>
29-
[LoggerMessage(EventId = 7485, Level = LogLevel.Error, Message = "Database error checking revocation status for JTI {Jti}")]
30-
public static partial void IsAccessTokenRevokedFailed(ILogger logger, Exception exception, string jti);
31+
[MessageLogging(EventId = 7485, Level = LogLevel.Error, Message = "Database error checking revocation status for JTI {jti}")]
32+
public static partial IGenericMessage IsAccessTokenRevokedFailed(ILogger logger, Exception exception, string jti);
3133

3234
/// <summary>Logs when cleaning up expired tokens fails.</summary>
33-
[LoggerMessage(EventId = 7486, Level = LogLevel.Error, Message = "Database error cleaning up expired tokens")]
34-
public static partial void CleanupExpiredTokensFailed(ILogger logger, Exception exception);
35+
[MessageLogging(EventId = 7486, Level = LogLevel.Error, Message = "Database error cleaning up expired tokens")]
36+
public static partial IGenericMessage CleanupExpiredTokensFailed(ILogger logger, Exception exception);
37+
38+
/// <summary>Logs when a user ID string cannot be parsed as a GUID.</summary>
39+
[MessageLogging(EventId = 7487, Level = LogLevel.Warning, Message = "Invalid user ID format: '{userId}'")]
40+
public static partial IGenericMessage InvalidUserId(ILogger logger, string userId);
41+
42+
/// <summary>Logs when the connection provider fails to resolve the named connection.</summary>
43+
[MessageLogging(EventId = 7488, Level = LogLevel.Error, Message = "Failed to resolve connection '{connectionName}' for token store")]
44+
public static partial IGenericMessage ConnectionFailed(ILogger logger, string connectionName);
45+
46+
/// <summary>Logs when the resolved connection is not a SQL Server connection.</summary>
47+
[MessageLogging(EventId = 7489, Level = LogLevel.Error, Message = "Connection '{connectionName}' is not a SQL Server connection")]
48+
public static partial IGenericMessage InvalidConnectionType(ILogger logger, string connectionName);
3549
}

0 commit comments

Comments
 (0)