feat(auth): ScadaBridge TransportExport excludes inbound API keys (re-arch C4; methods-only, import ignores legacy key sections); keys re-issued per environment

This commit is contained in:
Joseph Doherty
2026-06-02 05:06:40 -04:00
parent d1191fddf9
commit 731cfd3bfc
34 changed files with 212 additions and 190 deletions
@@ -13,6 +13,8 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
/// ignorant POCO types — what <see cref="EntitySerializer"/> consumes/produces
/// on the application side of the bundle boundary.
/// </summary>
// ApiKeys is intentionally absent: inbound API keys are not transported between
// environments (re-arch C4). The aggregate only carries API methods.
public sealed record EntityAggregate(
IReadOnlyList<TemplateFolder> TemplateFolders,
IReadOnlyList<Template> Templates,
@@ -22,7 +24,6 @@ public sealed record EntityAggregate(
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
IReadOnlyList<NotificationList> NotificationLists,
IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
IReadOnlyList<ApiKey> ApiKeys,
IReadOnlyList<ApiMethod> ApiMethods);
/// <summary>
@@ -30,6 +31,14 @@ public sealed record EntityAggregate(
/// order so importers can apply them inline. Lists are never null on the wire
/// — empty arrays are preferred over nulls so JSON consumers can rely on each
/// property being present.
/// <para>
/// <see cref="ApiKeys"/> is a <b>legacy, read-only</b> field retained purely for
/// backward-compatible deserialization of bundles produced before the
/// inbound-API-key re-architecture (C4). New exports never populate it (it stays
/// <c>null</c> and is dropped from the JSON by the serializer's
/// <c>WhenWritingNull</c> policy); the importer counts any keys present in an old
/// bundle, ignores them, and surfaces a note — keys are re-created per environment.
/// </para>
/// </summary>
public sealed record BundleContentDto(
IReadOnlyList<TemplateFolderDto> TemplateFolders,
@@ -39,8 +48,8 @@ public sealed record BundleContentDto(
IReadOnlyList<DatabaseConnectionDto> DatabaseConnections,
IReadOnlyList<NotificationListDto> NotificationLists,
IReadOnlyList<SmtpConfigDto> SmtpConfigs,
IReadOnlyList<ApiKeyDto> ApiKeys,
IReadOnlyList<ApiMethodDto> ApiMethods);
IReadOnlyList<ApiMethodDto> ApiMethods,
IReadOnlyList<ApiKeyDto>? ApiKeys = null);
/// <summary>
/// Carved-off secret values for an entity. The outer DTO carries all non-
@@ -144,16 +153,20 @@ public sealed record SmtpConfigDto(
TimeSpan RetryDelay,
SecretsBlock? Secrets);
// Legacy DTO: only deserialized from pre-C4 bundles so the importer can count and
// ignore the keys they contain. New exports never emit an ApiKeys array.
public sealed record ApiKeyDto(
string Name,
string KeyHash,
bool IsEnabled,
SecretsBlock? Secrets);
// ApprovedApiKeyIds is intentionally absent: it linked methods to keys that are no
// longer transported (re-arch C4). Scopes are re-granted per environment. The field
// in any old bundle is simply ignored on read.
public sealed record ApiMethodDto(
string Name,
string Script,
string? ApprovedApiKeyIds,
string? ParameterDefinitions,
string? ReturnDefinition,
int TimeoutSeconds);
@@ -153,17 +153,14 @@ public sealed class EntitySerializer
RetryDelay: smtp.RetryDelay,
Secrets: secrets);
}).ToList(),
// ApiKey stores only KeyHash already; no plaintext to carve. SecretsBlock
// stays null per design — KeyHash is on the public DTO.
ApiKeys: aggregate.ApiKeys.Select(k => new ApiKeyDto(
Name: k.Name,
KeyHash: k.KeyHash,
IsEnabled: k.IsEnabled,
Secrets: null)).ToList(),
// Inbound API keys are not transported between environments (re-arch C4):
// the bundle carries API methods only. ApiMethod.ApprovedApiKeyIds is also
// excluded — it references keys that aren't in the bundle; method→key scopes
// are re-granted per environment via the admin UI/CLI. The legacy ApiKeys
// field on the DTO stays null (and is dropped by WhenWritingNull).
ApiMethods: aggregate.ApiMethods.Select(m => new ApiMethodDto(
Name: m.Name,
Script: m.Script,
ApprovedApiKeyIds: m.ApprovedApiKeyIds,
ParameterDefinitions: m.ParameterDefinitions,
ReturnDefinition: m.ReturnDefinition,
TimeoutSeconds: m.TimeoutSeconds)).ToList());
@@ -336,21 +333,15 @@ public sealed class EntitySerializer
})
.ToList();
var apiKeys = content.ApiKeys
.Select((dto, ix) =>
{
var key = ApiKey.FromHash(dto.Name, dto.KeyHash);
key.Id = ix + 1;
key.IsEnabled = dto.IsEnabled;
return key;
})
.ToList();
// Inbound API keys are not transported (re-arch C4) — content.ApiKeys is a
// legacy field only present on pre-C4 bundles; it is ignored here. The
// BundleImporter is responsible for counting and reporting any such keys.
// ApiMethod.ApprovedApiKeyIds is likewise not reconstructed: scopes are
// re-granted per environment.
var apiMethods = content.ApiMethods
.Select((dto, ix) => new ApiMethod(dto.Name, dto.Script)
{
Id = ix + 1,
ApprovedApiKeyIds = dto.ApprovedApiKeyIds,
ParameterDefinitions = dto.ParameterDefinitions,
ReturnDefinition = dto.ReturnDefinition,
TimeoutSeconds = dto.TimeoutSeconds,
@@ -366,7 +357,6 @@ public sealed class EntitySerializer
DatabaseConnections: databaseConnections,
NotificationLists: notificationLists,
SmtpConfigurations: smtpConfigurations,
ApiKeys: apiKeys,
ApiMethods: apiMethods);
}
}