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:
@@ -69,7 +69,6 @@ public sealed class CompositionImportTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ public sealed class ConflictResolutionTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
|
||||
|
||||
@@ -132,7 +132,6 @@ public sealed class BundleExporterTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: true);
|
||||
|
||||
@@ -222,7 +221,6 @@ public sealed class BundleExporterTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: true);
|
||||
|
||||
|
||||
+93
-1
@@ -15,6 +15,7 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
|
||||
|
||||
@@ -96,7 +97,6 @@ public sealed class BundleImporterApplyTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
||||
@@ -747,4 +747,96 @@ public sealed class BundleImporterApplyTests : IDisposable
|
||||
}
|
||||
Assert.Equal(1, result.Overwritten);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Re-arch C4 backward-compat: a LEGACY (pre-C4) bundle still carries an
|
||||
// ApiKeys section. The importer must ignore those keys gracefully — it must
|
||||
// NOT fail to parse, must NOT create any ApiKey rows, and must surface the
|
||||
// ignored-key count on the result (so the operator knows to re-issue keys).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task ApplyAsync_ignores_legacy_api_keys_in_bundle_without_failing()
|
||||
{
|
||||
// Arrange: hand-pack a legacy bundle whose content JSON contains an
|
||||
// ApiKeys array plus one API method. New exports never emit ApiKeys, so
|
||||
// we build the BundleContentDto directly with a populated (non-null)
|
||||
// legacy ApiKeys list to faithfully simulate a pre-C4 file.
|
||||
var legacyContent = new BundleContentDto(
|
||||
TemplateFolders: Array.Empty<TemplateFolderDto>(),
|
||||
Templates: Array.Empty<TemplateDto>(),
|
||||
SharedScripts: Array.Empty<SharedScriptDto>(),
|
||||
ExternalSystems: Array.Empty<ExternalSystemDto>(),
|
||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||
ApiMethods: new[]
|
||||
{
|
||||
new ApiMethodDto("CreateOrder", "return 1;",
|
||||
ParameterDefinitions: null, ReturnDefinition: null, TimeoutSeconds: 30),
|
||||
},
|
||||
ApiKeys: new[]
|
||||
{
|
||||
new ApiKeyDto("legacy-key-a", "hash-a", IsEnabled: true, Secrets: null),
|
||||
new ApiKeyDto("legacy-key-b", "hash-b", IsEnabled: false, Secrets: null),
|
||||
});
|
||||
|
||||
Guid sessionId;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var serializer = scope.ServiceProvider.GetRequiredService<BundleSerializer>();
|
||||
var manifestBuilder = scope.ServiceProvider.GetRequiredService<ManifestBuilder>();
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
|
||||
var contentBytes = serializer.SerializeContentBytes(legacyContent);
|
||||
|
||||
// Sanity: the packed content really does carry an ApiKeys section, so
|
||||
// the test is exercising the legacy-ignore path rather than a no-op.
|
||||
var contentJson = System.Text.Encoding.UTF8.GetString(contentBytes);
|
||||
Assert.Contains("legacy-key-a", contentJson);
|
||||
|
||||
var manifest = manifestBuilder.Build(
|
||||
sourceEnvironment: "legacy-env",
|
||||
exportedBy: "alice",
|
||||
scadaBridgeVersion: "0.9.0",
|
||||
encryption: null,
|
||||
summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 1),
|
||||
contents: Array.Empty<ManifestContentEntry>(),
|
||||
contentBytes: contentBytes);
|
||||
await using var packed = serializer.Pack(legacyContent, manifest, passphrase: null, encryptor: null);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await packed.CopyToAsync(ms);
|
||||
ms.Position = 0;
|
||||
var session = await importer.LoadAsync(ms, passphrase: null);
|
||||
sessionId = session.SessionId;
|
||||
}
|
||||
|
||||
// Act — apply with a resolution only for the method; the keys carry no
|
||||
// resolutions (the preview never surfaces them) and must be ignored.
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution> { new("ApiMethod", "CreateOrder", ResolutionAction.Add, null) },
|
||||
user: "bob");
|
||||
}
|
||||
|
||||
// Assert — no keys created, the method WAS created, the ignored count is
|
||||
// surfaced, and the import did not fault.
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var inboundRepo = scope.ServiceProvider.GetRequiredService<IInboundApiRepository>();
|
||||
var keys = await inboundRepo.GetAllApiKeysAsync();
|
||||
Assert.Empty(keys);
|
||||
|
||||
var methods = await inboundRepo.GetAllApiMethodsAsync();
|
||||
Assert.Single(methods);
|
||||
Assert.Equal("CreateOrder", methods[0].Name);
|
||||
}
|
||||
|
||||
Assert.Equal(2, result.ApiKeysIgnored);
|
||||
Assert.Equal(1, result.Added); // the API method
|
||||
Assert.NotEqual(Guid.Empty, result.BundleImportId);
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -61,7 +61,6 @@ public sealed class BundleImporterPreviewTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
|
||||
|
||||
-1
@@ -181,7 +181,6 @@ public sealed class BundleImporterRollbackFailureTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
||||
|
||||
@@ -122,7 +122,6 @@ public sealed class RoundTripTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: notificationListIds,
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: true);
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ public sealed class SemanticValidatorImportTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
|
||||
|
||||
@@ -84,7 +84,6 @@ public sealed class ValidationFailureTests : IDisposable
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiKeyIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user