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:
@@ -61,7 +61,9 @@ public static class BundleCommands
|
|||||||
var dbConnectionsOption = NameListOption("--db-connections", "Comma-separated database-connection names");
|
var dbConnectionsOption = NameListOption("--db-connections", "Comma-separated database-connection names");
|
||||||
var notificationListsOption = NameListOption("--notification-lists", "Comma-separated notification-list names");
|
var notificationListsOption = NameListOption("--notification-lists", "Comma-separated notification-list names");
|
||||||
var smtpConfigsOption = NameListOption("--smtp-configs", "Comma-separated SMTP host names");
|
var smtpConfigsOption = NameListOption("--smtp-configs", "Comma-separated SMTP host names");
|
||||||
var apiKeysOption = NameListOption("--api-keys", "Comma-separated API-key names");
|
// Inbound API keys are not transported between environments (re-arch C4) — no
|
||||||
|
// --api-keys option. Re-create keys and re-grant their method scopes on the
|
||||||
|
// destination via the admin UI/CLI.
|
||||||
var apiMethodsOption = NameListOption("--api-methods", "Comma-separated API-method names");
|
var apiMethodsOption = NameListOption("--api-methods", "Comma-separated API-method names");
|
||||||
var includeDepsOption = new Option<bool>("--include-dependencies")
|
var includeDepsOption = new Option<bool>("--include-dependencies")
|
||||||
{
|
{
|
||||||
@@ -85,7 +87,6 @@ public static class BundleCommands
|
|||||||
cmd.Add(dbConnectionsOption);
|
cmd.Add(dbConnectionsOption);
|
||||||
cmd.Add(notificationListsOption);
|
cmd.Add(notificationListsOption);
|
||||||
cmd.Add(smtpConfigsOption);
|
cmd.Add(smtpConfigsOption);
|
||||||
cmd.Add(apiKeysOption);
|
|
||||||
cmd.Add(apiMethodsOption);
|
cmd.Add(apiMethodsOption);
|
||||||
cmd.Add(includeDepsOption);
|
cmd.Add(includeDepsOption);
|
||||||
cmd.Add(sourceEnvOption);
|
cmd.Add(sourceEnvOption);
|
||||||
@@ -106,7 +107,6 @@ public static class BundleCommands
|
|||||||
DatabaseConnectionNames: result.GetValue(dbConnectionsOption),
|
DatabaseConnectionNames: result.GetValue(dbConnectionsOption),
|
||||||
NotificationListNames: result.GetValue(notificationListsOption),
|
NotificationListNames: result.GetValue(notificationListsOption),
|
||||||
SmtpConfigurationNames: result.GetValue(smtpConfigsOption),
|
SmtpConfigurationNames: result.GetValue(smtpConfigsOption),
|
||||||
ApiKeyNames: result.GetValue(apiKeysOption),
|
|
||||||
ApiMethodNames: result.GetValue(apiMethodsOption),
|
ApiMethodNames: result.GetValue(apiMethodsOption),
|
||||||
IncludeDependencies: includeDeps,
|
IncludeDependencies: includeDeps,
|
||||||
Passphrase: passphrase,
|
Passphrase: passphrase,
|
||||||
|
|||||||
@@ -138,14 +138,15 @@
|
|||||||
@RenderCheckboxList(_smtpConfigs, s => s.Id, s => s.Host, _selectedSmtpConfigs)
|
@RenderCheckboxList(_smtpConfigs, s => s.Id, s => s.Host, _selectedSmtpConfigs)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="mb-4" data-testid="group-api-keys">
|
|
||||||
<legend class="h6">API Keys</legend>
|
|
||||||
@RenderCheckboxList(_apiKeys, k => k.Id, k => k.Name, _selectedApiKeys)
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="mb-4" data-testid="group-api-methods">
|
<fieldset class="mb-4" data-testid="group-api-methods">
|
||||||
<legend class="h6">API Methods</legend>
|
<legend class="h6">API Methods</legend>
|
||||||
@RenderCheckboxList(_apiMethods, m => m.Id, m => m.Name, _selectedApiMethods)
|
@RenderCheckboxList(_apiMethods, m => m.Id, m => m.Name, _selectedApiMethods)
|
||||||
|
<div class="alert alert-info small mt-2 mb-0 py-2" role="alert" data-testid="api-keys-not-transported">
|
||||||
|
<strong>API keys are not part of config transport.</strong> Inbound API keys
|
||||||
|
live in each environment's own secret store and cannot be exported. After
|
||||||
|
importing, re-create the keys on the destination and re-grant their method
|
||||||
|
scopes via the admin UI/CLI.
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
<div class="d-flex justify-content-end gap-2 mt-4">
|
||||||
@@ -261,10 +262,7 @@
|
|||||||
{
|
{
|
||||||
<li>SmtpConfig: @s.Host</li>
|
<li>SmtpConfig: @s.Host</li>
|
||||||
}
|
}
|
||||||
@foreach (var k in _resolved.ApiKeys.OrderBy(k => k.Name, StringComparer.OrdinalIgnoreCase))
|
@* Inbound API keys are not transported (re-arch C4) — methods only. *@
|
||||||
{
|
|
||||||
<li>ApiKey: @k.Name</li>
|
|
||||||
}
|
|
||||||
@foreach (var m in _resolved.ApiMethods.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
|
@foreach (var m in _resolved.ApiMethods.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
<li>ApiMethod: @m.Name</li>
|
<li>ApiMethod: @m.Name</li>
|
||||||
|
|||||||
+4
-6
@@ -69,7 +69,7 @@ public partial class TransportExport : ComponentBase
|
|||||||
private List<DatabaseConnectionDefinition> _dbConnections = new();
|
private List<DatabaseConnectionDefinition> _dbConnections = new();
|
||||||
private List<NotificationList> _notificationLists = new();
|
private List<NotificationList> _notificationLists = new();
|
||||||
private List<SmtpConfiguration> _smtpConfigs = new();
|
private List<SmtpConfiguration> _smtpConfigs = new();
|
||||||
private List<ApiKey> _apiKeys = new();
|
// Inbound API keys are not transported between environments (re-arch C4); only methods.
|
||||||
private List<ApiMethod> _apiMethods = new();
|
private List<ApiMethod> _apiMethods = new();
|
||||||
|
|
||||||
// ---- Step 1: selection state ----
|
// ---- Step 1: selection state ----
|
||||||
@@ -82,7 +82,7 @@ public partial class TransportExport : ComponentBase
|
|||||||
private readonly HashSet<int> _selectedDbConnections = new();
|
private readonly HashSet<int> _selectedDbConnections = new();
|
||||||
private readonly HashSet<int> _selectedNotificationLists = new();
|
private readonly HashSet<int> _selectedNotificationLists = new();
|
||||||
private readonly HashSet<int> _selectedSmtpConfigs = new();
|
private readonly HashSet<int> _selectedSmtpConfigs = new();
|
||||||
private readonly HashSet<int> _selectedApiKeys = new();
|
// No _selectedApiKeys: inbound API keys are not transported (re-arch C4).
|
||||||
private readonly HashSet<int> _selectedApiMethods = new();
|
private readonly HashSet<int> _selectedApiMethods = new();
|
||||||
private string _filter = string.Empty;
|
private string _filter = string.Empty;
|
||||||
private bool _includeDependencies = true;
|
private bool _includeDependencies = true;
|
||||||
@@ -124,7 +124,7 @@ public partial class TransportExport : ComponentBase
|
|||||||
_dbConnections = (await ExternalRepo.GetAllDatabaseConnectionsAsync()).ToList();
|
_dbConnections = (await ExternalRepo.GetAllDatabaseConnectionsAsync()).ToList();
|
||||||
_notificationLists = (await NotificationRepo.GetAllNotificationListsAsync()).ToList();
|
_notificationLists = (await NotificationRepo.GetAllNotificationListsAsync()).ToList();
|
||||||
_smtpConfigs = (await NotificationRepo.GetAllSmtpConfigurationsAsync()).ToList();
|
_smtpConfigs = (await NotificationRepo.GetAllSmtpConfigurationsAsync()).ToList();
|
||||||
_apiKeys = (await InboundApiRepo.GetAllApiKeysAsync()).ToList();
|
// Inbound API keys are not transported (re-arch C4) — only methods are loaded.
|
||||||
_apiMethods = (await InboundApiRepo.GetAllApiMethodsAsync()).ToList();
|
_apiMethods = (await InboundApiRepo.GetAllApiMethodsAsync()).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -169,7 +169,6 @@ public partial class TransportExport : ComponentBase
|
|||||||
|| _selectedDbConnections.Count > 0
|
|| _selectedDbConnections.Count > 0
|
||||||
|| _selectedNotificationLists.Count > 0
|
|| _selectedNotificationLists.Count > 0
|
||||||
|| _selectedSmtpConfigs.Count > 0
|
|| _selectedSmtpConfigs.Count > 0
|
||||||
|| _selectedApiKeys.Count > 0
|
|
||||||
|| _selectedApiMethods.Count > 0;
|
|| _selectedApiMethods.Count > 0;
|
||||||
|
|
||||||
private bool PassphraseValid =>
|
private bool PassphraseValid =>
|
||||||
@@ -205,7 +204,7 @@ public partial class TransportExport : ComponentBase
|
|||||||
DatabaseConnectionIds: _selectedDbConnections.ToList(),
|
DatabaseConnectionIds: _selectedDbConnections.ToList(),
|
||||||
NotificationListIds: _selectedNotificationLists.ToList(),
|
NotificationListIds: _selectedNotificationLists.ToList(),
|
||||||
SmtpConfigurationIds: _selectedSmtpConfigs.ToList(),
|
SmtpConfigurationIds: _selectedSmtpConfigs.ToList(),
|
||||||
ApiKeyIds: _selectedApiKeys.ToList(),
|
// Inbound API keys are not transported (re-arch C4) — methods only.
|
||||||
ApiMethodIds: _selectedApiMethods.ToList(),
|
ApiMethodIds: _selectedApiMethods.ToList(),
|
||||||
IncludeDependencies: _includeDependencies);
|
IncludeDependencies: _includeDependencies);
|
||||||
}
|
}
|
||||||
@@ -393,7 +392,6 @@ public partial class TransportExport : ComponentBase
|
|||||||
_selectedDbConnections.Clear();
|
_selectedDbConnections.Clear();
|
||||||
_selectedNotificationLists.Clear();
|
_selectedNotificationLists.Clear();
|
||||||
_selectedSmtpConfigs.Clear();
|
_selectedSmtpConfigs.Clear();
|
||||||
_selectedApiKeys.Clear();
|
|
||||||
_selectedApiMethods.Clear();
|
_selectedApiMethods.Clear();
|
||||||
_filter = string.Empty;
|
_filter = string.Empty;
|
||||||
_includeDependencies = true;
|
_includeDependencies = true;
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|||||||
/// Exports a bundle. Names rather than IDs in the selection so test scripts can
|
/// Exports a bundle. Names rather than IDs in the selection so test scripts can
|
||||||
/// be written without an ID lookup step. <c>All=true</c> overrides the per-type
|
/// be written without an ID lookup step. <c>All=true</c> overrides the per-type
|
||||||
/// name lists and exports every entity of every supported type.
|
/// name lists and exports every entity of every supported type.
|
||||||
|
/// <para>
|
||||||
|
/// Inbound API keys are intentionally not selectable: per the inbound-API-key
|
||||||
|
/// re-architecture (C4) keys are not transported between environments; only API
|
||||||
|
/// methods travel. Re-create keys and re-grant their method scopes on the
|
||||||
|
/// destination via the admin UI/CLI.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ExportBundleCommand(
|
public sealed record ExportBundleCommand(
|
||||||
bool All,
|
bool All,
|
||||||
@@ -15,7 +21,6 @@ public sealed record ExportBundleCommand(
|
|||||||
IReadOnlyList<string>? DatabaseConnectionNames,
|
IReadOnlyList<string>? DatabaseConnectionNames,
|
||||||
IReadOnlyList<string>? NotificationListNames,
|
IReadOnlyList<string>? NotificationListNames,
|
||||||
IReadOnlyList<string>? SmtpConfigurationNames,
|
IReadOnlyList<string>? SmtpConfigurationNames,
|
||||||
IReadOnlyList<string>? ApiKeyNames,
|
|
||||||
IReadOnlyList<string>? ApiMethodNames,
|
IReadOnlyList<string>? ApiMethodNames,
|
||||||
bool IncludeDependencies,
|
bool IncludeDependencies,
|
||||||
string? Passphrase,
|
string? Passphrase,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||||
|
|
||||||
|
// ApiKeys is intentionally absent: inbound API keys are not transported between
|
||||||
|
// environments (re-arch C4). Only API methods are summarised.
|
||||||
public sealed record BundleSummary(
|
public sealed record BundleSummary(
|
||||||
int Templates,
|
int Templates,
|
||||||
int TemplateFolders,
|
int TemplateFolders,
|
||||||
@@ -8,5 +10,4 @@ public sealed record BundleSummary(
|
|||||||
int DbConnections,
|
int DbConnections,
|
||||||
int NotificationLists,
|
int NotificationLists,
|
||||||
int SmtpConfigs,
|
int SmtpConfigs,
|
||||||
int ApiKeys,
|
|
||||||
int ApiMethods);
|
int ApiMethods);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||||
|
|
||||||
|
// Inbound API keys are intentionally absent from the transport selection: per the
|
||||||
|
// inbound-API-key re-architecture (commit C4) keys are NOT carried between
|
||||||
|
// environments. They live in the per-environment SQLite store (per-env pepper +
|
||||||
|
// secret-shown-once) and are re-created/re-granted via the admin UI/CLI on the
|
||||||
|
// destination. Only API *methods* travel in a bundle.
|
||||||
public sealed record ExportSelection(
|
public sealed record ExportSelection(
|
||||||
IReadOnlyList<int> TemplateIds,
|
IReadOnlyList<int> TemplateIds,
|
||||||
IReadOnlyList<int> SharedScriptIds,
|
IReadOnlyList<int> SharedScriptIds,
|
||||||
@@ -7,6 +12,5 @@ public sealed record ExportSelection(
|
|||||||
IReadOnlyList<int> DatabaseConnectionIds,
|
IReadOnlyList<int> DatabaseConnectionIds,
|
||||||
IReadOnlyList<int> NotificationListIds,
|
IReadOnlyList<int> NotificationListIds,
|
||||||
IReadOnlyList<int> SmtpConfigurationIds,
|
IReadOnlyList<int> SmtpConfigurationIds,
|
||||||
IReadOnlyList<int> ApiKeyIds,
|
|
||||||
IReadOnlyList<int> ApiMethodIds,
|
IReadOnlyList<int> ApiMethodIds,
|
||||||
bool IncludeDependencies);
|
bool IncludeDependencies);
|
||||||
|
|||||||
@@ -7,4 +7,8 @@ public sealed record ImportResult(
|
|||||||
int Skipped,
|
int Skipped,
|
||||||
int Renamed,
|
int Renamed,
|
||||||
IReadOnlyList<int> StaleInstanceIds,
|
IReadOnlyList<int> StaleInstanceIds,
|
||||||
string AuditEventCorrelation);
|
string AuditEventCorrelation,
|
||||||
|
// Number of legacy inbound API keys found in the bundle that were ignored
|
||||||
|
// (re-arch C4 — keys are not transported; re-create them on this environment).
|
||||||
|
// Defaults to 0 so existing positional construction sites stay source-compatible.
|
||||||
|
int ApiKeysIgnored = 0);
|
||||||
|
|||||||
@@ -1901,7 +1901,7 @@ public class ManagementActor : ReceiveActor
|
|||||||
var dbConnections = await externalRepo.GetAllDatabaseConnectionsAsync();
|
var dbConnections = await externalRepo.GetAllDatabaseConnectionsAsync();
|
||||||
var notificationLists = await notifRepo.GetAllNotificationListsAsync();
|
var notificationLists = await notifRepo.GetAllNotificationListsAsync();
|
||||||
var smtpConfigs = await notifRepo.GetAllSmtpConfigurationsAsync();
|
var smtpConfigs = await notifRepo.GetAllSmtpConfigurationsAsync();
|
||||||
var apiKeys = await inboundRepo.GetAllApiKeysAsync();
|
// Inbound API keys are not transported between environments (re-arch C4); only methods.
|
||||||
var apiMethods = await inboundRepo.GetAllApiMethodsAsync();
|
var apiMethods = await inboundRepo.GetAllApiMethodsAsync();
|
||||||
|
|
||||||
int[] ResolveIds<T>(IReadOnlyList<T> all, IReadOnlyList<string>? names,
|
int[] ResolveIds<T>(IReadOnlyList<T> all, IReadOnlyList<string>? names,
|
||||||
@@ -1931,7 +1931,6 @@ public class ManagementActor : ReceiveActor
|
|||||||
// SmtpConfiguration is keyed by Host (no Name column); the bundle
|
// SmtpConfiguration is keyed by Host (no Name column); the bundle
|
||||||
// preview row shows the Host value, so the CLI uses Host too.
|
// preview row shows the Host value, so the CLI uses Host too.
|
||||||
SmtpConfigurationIds: ResolveIds(smtpConfigs, cmd.SmtpConfigurationNames, s => s.Host, s => s.Id, "SMTP configuration"),
|
SmtpConfigurationIds: ResolveIds(smtpConfigs, cmd.SmtpConfigurationNames, s => s.Host, s => s.Id, "SMTP configuration"),
|
||||||
ApiKeyIds: ResolveIds(apiKeys, cmd.ApiKeyNames, k => k.Name, k => k.Id, "API key"),
|
|
||||||
ApiMethodIds: ResolveIds(apiMethods, cmd.ApiMethodNames, m => m.Name, m => m.Id, "API method"),
|
ApiMethodIds: ResolveIds(apiMethods, cmd.ApiMethodNames, m => m.Name, m => m.Id, "API method"),
|
||||||
IncludeDependencies: cmd.IncludeDependencies);
|
IncludeDependencies: cmd.IncludeDependencies);
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class BundleExporter : IBundleExporter
|
|||||||
DatabaseConnections: resolved.DatabaseConnections,
|
DatabaseConnections: resolved.DatabaseConnections,
|
||||||
NotificationLists: resolved.NotificationLists,
|
NotificationLists: resolved.NotificationLists,
|
||||||
SmtpConfigurations: resolved.SmtpConfigs,
|
SmtpConfigurations: resolved.SmtpConfigs,
|
||||||
ApiKeys: resolved.ApiKeys,
|
// Inbound API keys are not transported between environments (re-arch C4).
|
||||||
ApiMethods: resolved.ApiMethods);
|
ApiMethods: resolved.ApiMethods);
|
||||||
var contentDto = _entitySerializer.ToBundleContent(aggregate);
|
var contentDto = _entitySerializer.ToBundleContent(aggregate);
|
||||||
|
|
||||||
@@ -95,7 +95,6 @@ public sealed class BundleExporter : IBundleExporter
|
|||||||
DbConnections: resolved.DatabaseConnections.Count,
|
DbConnections: resolved.DatabaseConnections.Count,
|
||||||
NotificationLists: resolved.NotificationLists.Count,
|
NotificationLists: resolved.NotificationLists.Count,
|
||||||
SmtpConfigs: resolved.SmtpConfigs.Count,
|
SmtpConfigs: resolved.SmtpConfigs.Count,
|
||||||
ApiKeys: resolved.ApiKeys.Count,
|
|
||||||
ApiMethods: resolved.ApiMethods.Count);
|
ApiMethods: resolved.ApiMethods.Count);
|
||||||
|
|
||||||
// 4. Build a TEMPLATE manifest. BundleSerializer.Pack re-stamps both
|
// 4. Build a TEMPLATE manifest. BundleSerializer.Pack re-stamps both
|
||||||
|
|||||||
@@ -98,13 +98,9 @@ public sealed class DependencyResolver
|
|||||||
if (sm is not null) smtpConfigs[sm.Id] = sm;
|
if (sm is not null) smtpConfigs[sm.Id] = sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKeys = new Dictionary<int, ApiKey>();
|
// Inbound API keys are intentionally NOT resolved into the bundle: per the
|
||||||
foreach (var id in selection.ApiKeyIds.Distinct())
|
// inbound-API-key re-architecture (C4) keys are not transported between
|
||||||
{
|
// environments. Only API methods travel.
|
||||||
var k = await _inboundApi.GetApiKeyByIdAsync(id, ct).ConfigureAwait(false);
|
|
||||||
if (k is not null) apiKeys[k.Id] = k;
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiMethods = new Dictionary<int, ApiMethod>();
|
var apiMethods = new Dictionary<int, ApiMethod>();
|
||||||
foreach (var id in selection.ApiMethodIds.Distinct())
|
foreach (var id in selection.ApiMethodIds.Distinct())
|
||||||
{
|
{
|
||||||
@@ -148,7 +144,6 @@ public sealed class DependencyResolver
|
|||||||
dbConnections.Values,
|
dbConnections.Values,
|
||||||
notificationLists.Values,
|
notificationLists.Values,
|
||||||
smtpConfigs.Values,
|
smtpConfigs.Values,
|
||||||
apiKeys.Values,
|
|
||||||
apiMethods.Values);
|
apiMethods.Values);
|
||||||
|
|
||||||
return new ResolvedExport(
|
return new ResolvedExport(
|
||||||
@@ -160,7 +155,6 @@ public sealed class DependencyResolver
|
|||||||
DatabaseConnections: dbConnections.Values.OrderBy(d => d.Name, StringComparer.Ordinal).ToList(),
|
DatabaseConnections: dbConnections.Values.OrderBy(d => d.Name, StringComparer.Ordinal).ToList(),
|
||||||
NotificationLists: notificationLists.Values.OrderBy(n => n.Name, StringComparer.Ordinal).ToList(),
|
NotificationLists: notificationLists.Values.OrderBy(n => n.Name, StringComparer.Ordinal).ToList(),
|
||||||
SmtpConfigs: smtpConfigs.Values.OrderBy(s => s.Host, StringComparer.Ordinal).ToList(),
|
SmtpConfigs: smtpConfigs.Values.OrderBy(s => s.Host, StringComparer.Ordinal).ToList(),
|
||||||
ApiKeys: apiKeys.Values.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(),
|
|
||||||
ApiMethods: apiMethods.Values.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(),
|
ApiMethods: apiMethods.Values.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(),
|
||||||
ContentManifest: manifest);
|
ContentManifest: manifest);
|
||||||
}
|
}
|
||||||
@@ -368,7 +362,6 @@ public sealed class DependencyResolver
|
|||||||
IEnumerable<DatabaseConnectionDefinition> dbConnections,
|
IEnumerable<DatabaseConnectionDefinition> dbConnections,
|
||||||
IEnumerable<NotificationList> notificationLists,
|
IEnumerable<NotificationList> notificationLists,
|
||||||
IEnumerable<SmtpConfiguration> smtpConfigs,
|
IEnumerable<SmtpConfiguration> smtpConfigs,
|
||||||
IEnumerable<ApiKey> apiKeys,
|
|
||||||
IEnumerable<ApiMethod> apiMethods)
|
IEnumerable<ApiMethod> apiMethods)
|
||||||
{
|
{
|
||||||
var entries = new List<ManifestContentEntry>();
|
var entries = new List<ManifestContentEntry>();
|
||||||
@@ -419,10 +412,7 @@ public sealed class DependencyResolver
|
|||||||
{
|
{
|
||||||
entries.Add(new ManifestContentEntry("SmtpConfiguration", s.Host, 1, Array.Empty<string>()));
|
entries.Add(new ManifestContentEntry("SmtpConfiguration", s.Host, 1, Array.Empty<string>()));
|
||||||
}
|
}
|
||||||
foreach (var k in apiKeys.OrderBy(x => x.Name, StringComparer.Ordinal))
|
// Inbound API keys are not transported (re-arch C4) — no ApiKey manifest entries.
|
||||||
{
|
|
||||||
entries.Add(new ManifestContentEntry("ApiKey", k.Name, 1, Array.Empty<string>()));
|
|
||||||
}
|
|
||||||
foreach (var m in apiMethods.OrderBy(x => x.Name, StringComparer.Ordinal))
|
foreach (var m in apiMethods.OrderBy(x => x.Name, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
entries.Add(new ManifestContentEntry("ApiMethod", m.Name, 1, Array.Empty<string>()));
|
entries.Add(new ManifestContentEntry("ApiMethod", m.Name, 1, Array.Empty<string>()));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; // ApiMethod
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||||
@@ -23,6 +23,6 @@ public sealed record ResolvedExport(
|
|||||||
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
||||||
IReadOnlyList<NotificationList> NotificationLists,
|
IReadOnlyList<NotificationList> NotificationLists,
|
||||||
IReadOnlyList<SmtpConfiguration> SmtpConfigs,
|
IReadOnlyList<SmtpConfiguration> SmtpConfigs,
|
||||||
IReadOnlyList<ApiKey> ApiKeys,
|
// Inbound API keys are not transported between environments (re-arch C4); only methods.
|
||||||
IReadOnlyList<ApiMethod> ApiMethods,
|
IReadOnlyList<ApiMethod> ApiMethods,
|
||||||
IReadOnlyList<ManifestContentEntry> ContentManifest);
|
IReadOnlyList<ManifestContentEntry> ContentManifest);
|
||||||
|
|||||||
@@ -244,26 +244,8 @@ public sealed class ArtifactDiff
|
|||||||
return BuildItem("SmtpConfiguration", incoming.Host, changes);
|
return BuildItem("SmtpConfiguration", incoming.Host, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// CompareApiKey was removed in re-arch C4: inbound API keys are not transported
|
||||||
/// Compares an incoming API key against the existing API key in the database.
|
// between environments, so the import preview never diffs keys.
|
||||||
/// </summary>
|
|
||||||
/// <param name="incoming">The incoming API key from the bundle.</param>
|
|
||||||
/// <param name="existing">The existing API key in the database, or null if new.</param>
|
|
||||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
|
||||||
public ImportPreviewItem CompareApiKey(ApiKeyDto incoming, ApiKey? existing)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(incoming);
|
|
||||||
if (existing is null) return New("ApiKey", incoming.Name);
|
|
||||||
|
|
||||||
var changes = new List<FieldChange>();
|
|
||||||
AddIfDifferent(changes, "IsEnabled", existing.IsEnabled, incoming.IsEnabled);
|
|
||||||
// KeyHash is opaque — record only changed/unchanged, not the value.
|
|
||||||
if (!string.Equals(existing.KeyHash, incoming.KeyHash, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
changes.Add(new FieldChange("KeyHash", "<changed>", "<changed>"));
|
|
||||||
}
|
|
||||||
return BuildItem("ApiKey", incoming.Name, changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compares an incoming API method against the existing API method in the database.
|
/// Compares an incoming API method against the existing API method in the database.
|
||||||
@@ -277,7 +259,7 @@ public sealed class ArtifactDiff
|
|||||||
if (existing is null) return New("ApiMethod", incoming.Name);
|
if (existing is null) return New("ApiMethod", incoming.Name);
|
||||||
|
|
||||||
var changes = new List<FieldChange>();
|
var changes = new List<FieldChange>();
|
||||||
AddIfDifferent(changes, "ApprovedApiKeyIds", existing.ApprovedApiKeyIds, incoming.ApprovedApiKeyIds);
|
// ApprovedApiKeyIds is not transported (re-arch C4) and is excluded from the diff.
|
||||||
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
||||||
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
||||||
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds);
|
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds);
|
||||||
|
|||||||
@@ -408,14 +408,12 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
items.Add(_diff.CompareSmtpConfiguration(sm, existing));
|
items.Add(_diff.CompareSmtpConfiguration(sm, existing));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- ApiKeys (no by-name lookup — scan GetAll) ----
|
// ---- ApiKeys ----
|
||||||
var allApiKeys = await _inboundApiRepo.GetAllApiKeysAsync(ct).ConfigureAwait(false);
|
// Inbound API keys are not transported between environments (re-arch C4).
|
||||||
var apiKeyByName = allApiKeys.ToDictionary(k => k.Name, k => k, StringComparer.Ordinal);
|
// New bundles never carry a keys section. A pre-C4 bundle may still contain
|
||||||
foreach (var k in content.ApiKeys)
|
// one; we do NOT surface those keys as importable preview rows (they would
|
||||||
{
|
// offer Add/Overwrite actions for keys that can't be meaningfully re-created
|
||||||
apiKeyByName.TryGetValue(k.Name, out var existing);
|
// from a hash). They are counted, ignored, and reported at apply time.
|
||||||
items.Add(_diff.CompareApiKey(k, existing));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- ApiMethods ----
|
// ---- ApiMethods ----
|
||||||
foreach (var m in content.ApiMethods)
|
foreach (var m in content.ApiMethods)
|
||||||
@@ -617,6 +615,12 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
r => r);
|
r => r);
|
||||||
var summary = new ImportSummary();
|
var summary = new ImportSummary();
|
||||||
|
|
||||||
|
// Inbound API keys are not transported between environments (re-arch C4).
|
||||||
|
// A pre-C4 bundle may still contain a keys section; we ignore those keys
|
||||||
|
// entirely (never re-create them) but count them so the result can tell
|
||||||
|
// the operator to re-issue keys on this environment.
|
||||||
|
var apiKeysIgnored = content.ApiKeys?.Count ?? 0;
|
||||||
|
|
||||||
// Set the correlation BEFORE the transaction so any audit writes
|
// Set the correlation BEFORE the transaction so any audit writes
|
||||||
// triggered during the apply pick up the BundleImportId — AuditService
|
// triggered during the apply pick up the BundleImportId — AuditService
|
||||||
// reads the scoped context at the moment LogAsync is called.
|
// reads the scoped context at the moment LogAsync is called.
|
||||||
@@ -660,7 +664,8 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
await ApplyDatabaseConnectionsAsync(content.DatabaseConnections, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
await ApplyDatabaseConnectionsAsync(content.DatabaseConnections, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||||
await ApplyNotificationListsAsync(content.NotificationLists, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
await ApplyNotificationListsAsync(content.NotificationLists, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||||
await ApplySmtpConfigsAsync(content.SmtpConfigs, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
await ApplySmtpConfigsAsync(content.SmtpConfigs, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||||
await ApplyApiKeysAsync(content.ApiKeys, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
// Inbound API keys are NOT applied from a bundle (re-arch C4) — any keys
|
||||||
|
// in a legacy bundle were counted above (apiKeysIgnored) and are skipped.
|
||||||
await ApplyApiMethodsAsync(content.ApiMethods, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
await ApplyApiMethodsAsync(content.ApiMethods, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
// FU-B / #39 + remainder of #37 — second-pass rewire of name-keyed
|
// FU-B / #39 + remainder of #37 — second-pass rewire of name-keyed
|
||||||
@@ -700,6 +705,9 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
summary.Skipped,
|
summary.Skipped,
|
||||||
summary.Renamed,
|
summary.Renamed,
|
||||||
},
|
},
|
||||||
|
// re-arch C4: legacy inbound API keys present in the bundle that
|
||||||
|
// were ignored (not transported / not re-created here).
|
||||||
|
ApiKeysIgnored = apiKeysIgnored,
|
||||||
},
|
},
|
||||||
cancellationToken: ct).ConfigureAwait(false);
|
cancellationToken: ct).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -721,7 +729,8 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
Skipped: summary.Skipped,
|
Skipped: summary.Skipped,
|
||||||
Renamed: summary.Renamed,
|
Renamed: summary.Renamed,
|
||||||
StaleInstanceIds: Array.Empty<int>(),
|
StaleInstanceIds: Array.Empty<int>(),
|
||||||
AuditEventCorrelation: bundleImportId.ToString());
|
AuditEventCorrelation: bundleImportId.ToString(),
|
||||||
|
ApiKeysIgnored: apiKeysIgnored);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2015,59 +2024,9 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
target.Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null;
|
target.Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyApiKeysAsync(
|
// ApplyApiKeysAsync was removed in re-arch C4: inbound API keys are not
|
||||||
IReadOnlyList<ApiKeyDto> dtos,
|
// transported between environments, so a bundle never re-creates keys. Any keys
|
||||||
Dictionary<(string, string), ImportResolution> map,
|
// present in a legacy (pre-C4) bundle are counted and ignored in ApplyAsync.
|
||||||
string user,
|
|
||||||
ImportSummary summary,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (dtos.Count == 0) return;
|
|
||||||
var all = await _inboundApiRepo.GetAllApiKeysAsync(ct).ConfigureAwait(false);
|
|
||||||
var byName = all.ToDictionary(k => k.Name, k => k, StringComparer.Ordinal);
|
|
||||||
|
|
||||||
foreach (var dto in dtos)
|
|
||||||
{
|
|
||||||
var resolution = ResolveOrDefault(map, "ApiKey", dto.Name);
|
|
||||||
switch (resolution.Action)
|
|
||||||
{
|
|
||||||
case ResolutionAction.Skip:
|
|
||||||
summary.Skipped++;
|
|
||||||
break;
|
|
||||||
case ResolutionAction.Rename:
|
|
||||||
{
|
|
||||||
var name = resolution.RenameTo ?? dto.Name;
|
|
||||||
var key = ApiKey.FromHash(name, dto.KeyHash);
|
|
||||||
key.IsEnabled = dto.IsEnabled;
|
|
||||||
await _inboundApiRepo.AddApiKeyAsync(key, ct).ConfigureAwait(false);
|
|
||||||
await _auditService.LogAsync(user, "Create", "ApiKey", "0", name,
|
|
||||||
new { key.Name, RenamedFrom = dto.Name }, ct).ConfigureAwait(false);
|
|
||||||
summary.Renamed++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ResolutionAction.Overwrite when byName.TryGetValue(dto.Name, out var ex):
|
|
||||||
ex.KeyHash = dto.KeyHash;
|
|
||||||
ex.IsEnabled = dto.IsEnabled;
|
|
||||||
await _inboundApiRepo.UpdateApiKeyAsync(ex, ct).ConfigureAwait(false);
|
|
||||||
await _auditService.LogAsync(user, "Update", "ApiKey", ex.Id.ToString(), ex.Name,
|
|
||||||
new { ex.Name, ex.IsEnabled }, ct).ConfigureAwait(false);
|
|
||||||
summary.Overwritten++;
|
|
||||||
break;
|
|
||||||
case ResolutionAction.Add:
|
|
||||||
case ResolutionAction.Overwrite:
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
var key = ApiKey.FromHash(dto.Name, dto.KeyHash);
|
|
||||||
key.IsEnabled = dto.IsEnabled;
|
|
||||||
await _inboundApiRepo.AddApiKeyAsync(key, ct).ConfigureAwait(false);
|
|
||||||
await _auditService.LogAsync(user, "Create", "ApiKey", "0", key.Name,
|
|
||||||
new { key.Name, key.IsEnabled }, ct).ConfigureAwait(false);
|
|
||||||
summary.Added++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyApiMethodsAsync(
|
private async Task ApplyApiMethodsAsync(
|
||||||
IReadOnlyList<ApiMethodDto> dtos,
|
IReadOnlyList<ApiMethodDto> dtos,
|
||||||
@@ -2098,7 +2057,9 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
}
|
}
|
||||||
case ResolutionAction.Overwrite when existing is not null:
|
case ResolutionAction.Overwrite when existing is not null:
|
||||||
existing.Script = dto.Script;
|
existing.Script = dto.Script;
|
||||||
existing.ApprovedApiKeyIds = dto.ApprovedApiKeyIds;
|
// ApprovedApiKeyIds is NOT overwritten from a bundle (re-arch C4):
|
||||||
|
// method→key scopes are re-granted per environment and any value on
|
||||||
|
// the target row is preserved across an import.
|
||||||
existing.ParameterDefinitions = dto.ParameterDefinitions;
|
existing.ParameterDefinitions = dto.ParameterDefinitions;
|
||||||
existing.ReturnDefinition = dto.ReturnDefinition;
|
existing.ReturnDefinition = dto.ReturnDefinition;
|
||||||
existing.TimeoutSeconds = dto.TimeoutSeconds;
|
existing.TimeoutSeconds = dto.TimeoutSeconds;
|
||||||
@@ -2124,9 +2085,10 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
|
|
||||||
private static ApiMethod BuildApiMethod(ApiMethodDto dto, string? overrideName)
|
private static ApiMethod BuildApiMethod(ApiMethodDto dto, string? overrideName)
|
||||||
{
|
{
|
||||||
|
// ApprovedApiKeyIds is intentionally left at its default (null): keys are not
|
||||||
|
// transported (re-arch C4) and method→key scopes are re-granted per environment.
|
||||||
return new ApiMethod(overrideName ?? dto.Name, dto.Script)
|
return new ApiMethod(overrideName ?? dto.Name, dto.Script)
|
||||||
{
|
{
|
||||||
ApprovedApiKeyIds = dto.ApprovedApiKeyIds,
|
|
||||||
ParameterDefinitions = dto.ParameterDefinitions,
|
ParameterDefinitions = dto.ParameterDefinitions,
|
||||||
ReturnDefinition = dto.ReturnDefinition,
|
ReturnDefinition = dto.ReturnDefinition,
|
||||||
TimeoutSeconds = dto.TimeoutSeconds,
|
TimeoutSeconds = dto.TimeoutSeconds,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
|||||||
/// ignorant POCO types — what <see cref="EntitySerializer"/> consumes/produces
|
/// ignorant POCO types — what <see cref="EntitySerializer"/> consumes/produces
|
||||||
/// on the application side of the bundle boundary.
|
/// on the application side of the bundle boundary.
|
||||||
/// </summary>
|
/// </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(
|
public sealed record EntityAggregate(
|
||||||
IReadOnlyList<TemplateFolder> TemplateFolders,
|
IReadOnlyList<TemplateFolder> TemplateFolders,
|
||||||
IReadOnlyList<Template> Templates,
|
IReadOnlyList<Template> Templates,
|
||||||
@@ -22,7 +24,6 @@ public sealed record EntityAggregate(
|
|||||||
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
||||||
IReadOnlyList<NotificationList> NotificationLists,
|
IReadOnlyList<NotificationList> NotificationLists,
|
||||||
IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
|
IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
|
||||||
IReadOnlyList<ApiKey> ApiKeys,
|
|
||||||
IReadOnlyList<ApiMethod> ApiMethods);
|
IReadOnlyList<ApiMethod> ApiMethods);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,6 +31,14 @@ public sealed record EntityAggregate(
|
|||||||
/// order so importers can apply them inline. Lists are never null on the wire
|
/// 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
|
/// — empty arrays are preferred over nulls so JSON consumers can rely on each
|
||||||
/// property being present.
|
/// 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>
|
/// </summary>
|
||||||
public sealed record BundleContentDto(
|
public sealed record BundleContentDto(
|
||||||
IReadOnlyList<TemplateFolderDto> TemplateFolders,
|
IReadOnlyList<TemplateFolderDto> TemplateFolders,
|
||||||
@@ -39,8 +48,8 @@ public sealed record BundleContentDto(
|
|||||||
IReadOnlyList<DatabaseConnectionDto> DatabaseConnections,
|
IReadOnlyList<DatabaseConnectionDto> DatabaseConnections,
|
||||||
IReadOnlyList<NotificationListDto> NotificationLists,
|
IReadOnlyList<NotificationListDto> NotificationLists,
|
||||||
IReadOnlyList<SmtpConfigDto> SmtpConfigs,
|
IReadOnlyList<SmtpConfigDto> SmtpConfigs,
|
||||||
IReadOnlyList<ApiKeyDto> ApiKeys,
|
IReadOnlyList<ApiMethodDto> ApiMethods,
|
||||||
IReadOnlyList<ApiMethodDto> ApiMethods);
|
IReadOnlyList<ApiKeyDto>? ApiKeys = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Carved-off secret values for an entity. The outer DTO carries all non-
|
/// Carved-off secret values for an entity. The outer DTO carries all non-
|
||||||
@@ -144,16 +153,20 @@ public sealed record SmtpConfigDto(
|
|||||||
TimeSpan RetryDelay,
|
TimeSpan RetryDelay,
|
||||||
SecretsBlock? Secrets);
|
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(
|
public sealed record ApiKeyDto(
|
||||||
string Name,
|
string Name,
|
||||||
string KeyHash,
|
string KeyHash,
|
||||||
bool IsEnabled,
|
bool IsEnabled,
|
||||||
SecretsBlock? Secrets);
|
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(
|
public sealed record ApiMethodDto(
|
||||||
string Name,
|
string Name,
|
||||||
string Script,
|
string Script,
|
||||||
string? ApprovedApiKeyIds,
|
|
||||||
string? ParameterDefinitions,
|
string? ParameterDefinitions,
|
||||||
string? ReturnDefinition,
|
string? ReturnDefinition,
|
||||||
int TimeoutSeconds);
|
int TimeoutSeconds);
|
||||||
|
|||||||
@@ -153,17 +153,14 @@ public sealed class EntitySerializer
|
|||||||
RetryDelay: smtp.RetryDelay,
|
RetryDelay: smtp.RetryDelay,
|
||||||
Secrets: secrets);
|
Secrets: secrets);
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
// ApiKey stores only KeyHash already; no plaintext to carve. SecretsBlock
|
// Inbound API keys are not transported between environments (re-arch C4):
|
||||||
// stays null per design — KeyHash is on the public DTO.
|
// the bundle carries API methods only. ApiMethod.ApprovedApiKeyIds is also
|
||||||
ApiKeys: aggregate.ApiKeys.Select(k => new ApiKeyDto(
|
// excluded — it references keys that aren't in the bundle; method→key scopes
|
||||||
Name: k.Name,
|
// are re-granted per environment via the admin UI/CLI. The legacy ApiKeys
|
||||||
KeyHash: k.KeyHash,
|
// field on the DTO stays null (and is dropped by WhenWritingNull).
|
||||||
IsEnabled: k.IsEnabled,
|
|
||||||
Secrets: null)).ToList(),
|
|
||||||
ApiMethods: aggregate.ApiMethods.Select(m => new ApiMethodDto(
|
ApiMethods: aggregate.ApiMethods.Select(m => new ApiMethodDto(
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Script: m.Script,
|
Script: m.Script,
|
||||||
ApprovedApiKeyIds: m.ApprovedApiKeyIds,
|
|
||||||
ParameterDefinitions: m.ParameterDefinitions,
|
ParameterDefinitions: m.ParameterDefinitions,
|
||||||
ReturnDefinition: m.ReturnDefinition,
|
ReturnDefinition: m.ReturnDefinition,
|
||||||
TimeoutSeconds: m.TimeoutSeconds)).ToList());
|
TimeoutSeconds: m.TimeoutSeconds)).ToList());
|
||||||
@@ -336,21 +333,15 @@ public sealed class EntitySerializer
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var apiKeys = content.ApiKeys
|
// Inbound API keys are not transported (re-arch C4) — content.ApiKeys is a
|
||||||
.Select((dto, ix) =>
|
// legacy field only present on pre-C4 bundles; it is ignored here. The
|
||||||
{
|
// BundleImporter is responsible for counting and reporting any such keys.
|
||||||
var key = ApiKey.FromHash(dto.Name, dto.KeyHash);
|
// ApiMethod.ApprovedApiKeyIds is likewise not reconstructed: scopes are
|
||||||
key.Id = ix + 1;
|
// re-granted per environment.
|
||||||
key.IsEnabled = dto.IsEnabled;
|
|
||||||
return key;
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var apiMethods = content.ApiMethods
|
var apiMethods = content.ApiMethods
|
||||||
.Select((dto, ix) => new ApiMethod(dto.Name, dto.Script)
|
.Select((dto, ix) => new ApiMethod(dto.Name, dto.Script)
|
||||||
{
|
{
|
||||||
Id = ix + 1,
|
Id = ix + 1,
|
||||||
ApprovedApiKeyIds = dto.ApprovedApiKeyIds,
|
|
||||||
ParameterDefinitions = dto.ParameterDefinitions,
|
ParameterDefinitions = dto.ParameterDefinitions,
|
||||||
ReturnDefinition = dto.ReturnDefinition,
|
ReturnDefinition = dto.ReturnDefinition,
|
||||||
TimeoutSeconds = dto.TimeoutSeconds,
|
TimeoutSeconds = dto.TimeoutSeconds,
|
||||||
@@ -366,7 +357,6 @@ public sealed class EntitySerializer
|
|||||||
DatabaseConnections: databaseConnections,
|
DatabaseConnections: databaseConnections,
|
||||||
NotificationLists: notificationLists,
|
NotificationLists: notificationLists,
|
||||||
SmtpConfigurations: smtpConfigurations,
|
SmtpConfigurations: smtpConfigurations,
|
||||||
ApiKeys: apiKeys,
|
|
||||||
ApiMethods: apiMethods);
|
ApiMethods: apiMethods);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -118,7 +118,8 @@ public class TransportExportPageTests : BunitContext
|
|||||||
var db = new DatabaseConnectionDefinition("Hist", "Server=.;") { Id = 30 };
|
var db = new DatabaseConnectionDefinition("Hist", "Server=.;") { Id = 30 };
|
||||||
var notifList = new NotificationList("Ops") { Id = 40 };
|
var notifList = new NotificationList("Ops") { Id = 40 };
|
||||||
var smtp = new SmtpConfiguration("smtp.example.com", "Basic", "no-reply@example.com") { Id = 50 };
|
var smtp = new SmtpConfiguration("smtp.example.com", "Basic", "no-reply@example.com") { Id = 50 };
|
||||||
var apiKey = new ApiKey("ext-system", "key-hash") { Id = 60 };
|
// Inbound API keys are not transported between environments (re-arch C4) — the
|
||||||
|
// export page no longer offers a keys selection list, only API methods.
|
||||||
var apiMethod = new ApiMethod("CreateOrder", "// noop") { Id = 70 };
|
var apiMethod = new ApiMethod("CreateOrder", "// noop") { Id = 70 };
|
||||||
|
|
||||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||||
@@ -135,15 +136,14 @@ public class TransportExportPageTests : BunitContext
|
|||||||
.Returns(Task.FromResult<IReadOnlyList<NotificationList>>(new List<NotificationList> { notifList }));
|
.Returns(Task.FromResult<IReadOnlyList<NotificationList>>(new List<NotificationList> { notifList }));
|
||||||
_notificationRepo.GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>())
|
_notificationRepo.GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(new List<SmtpConfiguration> { smtp }));
|
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(new List<SmtpConfiguration> { smtp }));
|
||||||
_inboundApiRepo.GetAllApiKeysAsync(Arg.Any<CancellationToken>())
|
|
||||||
.Returns(Task.FromResult<IReadOnlyList<ApiKey>>(new List<ApiKey> { apiKey }));
|
|
||||||
_inboundApiRepo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
|
_inboundApiRepo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult<IReadOnlyList<ApiMethod>>(new List<ApiMethod> { apiMethod }));
|
.Returns(Task.FromResult<IReadOnlyList<ApiMethod>>(new List<ApiMethod> { apiMethod }));
|
||||||
|
|
||||||
var cut = Render<TransportExportPage>();
|
var cut = Render<TransportExportPage>();
|
||||||
cut.WaitForState(() => cut.Markup.Contains("Pump"));
|
cut.WaitForState(() => cut.Markup.Contains("Pump"));
|
||||||
|
|
||||||
// All six flat groups (plus templates) are present.
|
// All flat groups (plus templates) are present. There is intentionally NO
|
||||||
|
// API-keys group: inbound API keys are not transported (re-arch C4).
|
||||||
foreach (var groupId in new[]
|
foreach (var groupId in new[]
|
||||||
{
|
{
|
||||||
"group-templates",
|
"group-templates",
|
||||||
@@ -152,20 +152,23 @@ public class TransportExportPageTests : BunitContext
|
|||||||
"group-db-connections",
|
"group-db-connections",
|
||||||
"group-notification-lists",
|
"group-notification-lists",
|
||||||
"group-smtp-configs",
|
"group-smtp-configs",
|
||||||
"group-api-keys",
|
|
||||||
"group-api-methods",
|
"group-api-methods",
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Assert.NotNull(cut.Find($"[data-testid='{groupId}']"));
|
Assert.NotNull(cut.Find($"[data-testid='{groupId}']"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The API-keys selection group is gone, replaced by an info note explaining
|
||||||
|
// that keys must be re-created per environment.
|
||||||
|
Assert.Empty(cut.FindAll("[data-testid='group-api-keys']"));
|
||||||
|
Assert.NotNull(cut.Find("[data-testid='api-keys-not-transported']"));
|
||||||
|
|
||||||
// Sanity: each artifact shows its label.
|
// Sanity: each artifact shows its label.
|
||||||
Assert.Contains("Helpers", cut.Markup);
|
Assert.Contains("Helpers", cut.Markup);
|
||||||
Assert.Contains("ERP", cut.Markup);
|
Assert.Contains("ERP", cut.Markup);
|
||||||
Assert.Contains("Hist", cut.Markup);
|
Assert.Contains("Hist", cut.Markup);
|
||||||
Assert.Contains("Ops", cut.Markup);
|
Assert.Contains("Ops", cut.Markup);
|
||||||
Assert.Contains("smtp.example.com", cut.Markup);
|
Assert.Contains("smtp.example.com", cut.Markup);
|
||||||
Assert.Contains("ext-system", cut.Markup);
|
|
||||||
Assert.Contains("CreateOrder", cut.Markup);
|
Assert.Contains("CreateOrder", cut.Markup);
|
||||||
|
|
||||||
// Next button is disabled while no selection exists.
|
// Next button is disabled while no selection exists.
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ public class TransportImportPageTests : BunitContext
|
|||||||
Iterations: 600_000,
|
Iterations: 600_000,
|
||||||
SaltB64: "abc",
|
SaltB64: "abc",
|
||||||
IvB64: "def"),
|
IvB64: "def"),
|
||||||
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0, 0),
|
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
Contents: Array.Empty<ManifestContentEntry>()),
|
Contents: Array.Empty<ManifestContentEntry>()),
|
||||||
DecryptedContent = Array.Empty<byte>(),
|
DecryptedContent = Array.Empty<byte>(),
|
||||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class TransportRecordsTests
|
|||||||
var summary = new BundleSummary(
|
var summary = new BundleSummary(
|
||||||
Templates: 2, TemplateFolders: 1, SharedScripts: 3,
|
Templates: 2, TemplateFolders: 1, SharedScripts: 3,
|
||||||
ExternalSystems: 1, DbConnections: 0, NotificationLists: 1,
|
ExternalSystems: 1, DbConnections: 0, NotificationLists: 1,
|
||||||
SmtpConfigs: 1, ApiKeys: 0, ApiMethods: 4);
|
SmtpConfigs: 1, ApiMethods: 4);
|
||||||
var contents = new List<ManifestContentEntry>
|
var contents = new List<ManifestContentEntry>
|
||||||
{
|
{
|
||||||
new("Template", "Pump", 1, new List<string> { "Shared.Helpers" }),
|
new("Template", "Pump", 1, new List<string> { "Shared.Helpers" }),
|
||||||
@@ -71,7 +71,7 @@ public sealed class TransportRecordsTests
|
|||||||
Iterations: 600_000,
|
Iterations: 600_000,
|
||||||
SaltB64: "c2FsdA==",
|
SaltB64: "c2FsdA==",
|
||||||
IvB64: "aXY=");
|
IvB64: "aXY=");
|
||||||
var summary = new BundleSummary(1, 0, 0, 0, 0, 0, 0, 0, 0);
|
var summary = new BundleSummary(1, 0, 0, 0, 0, 0, 0, 0);
|
||||||
var manifest = new BundleManifest(
|
var manifest = new BundleManifest(
|
||||||
BundleFormatVersion: 1,
|
BundleFormatVersion: 1,
|
||||||
SchemaVersion: "1.0",
|
SchemaVersion: "1.0",
|
||||||
@@ -116,7 +116,6 @@ public sealed class TransportRecordsTests
|
|||||||
DatabaseConnectionIds: new[] { 20, 21 },
|
DatabaseConnectionIds: new[] { 20, 21 },
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: new[] { 30 },
|
SmtpConfigurationIds: new[] { 30 },
|
||||||
ApiKeyIds: new[] { 40, 41 },
|
|
||||||
ApiMethodIds: new[] { 50 },
|
ApiMethodIds: new[] { 50 },
|
||||||
IncludeDependencies: true);
|
IncludeDependencies: true);
|
||||||
|
|
||||||
@@ -137,7 +136,6 @@ public sealed class TransportRecordsTests
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
@@ -1220,7 +1220,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
|||||||
TemplateNames: null, SharedScriptNames: null,
|
TemplateNames: null, SharedScriptNames: null,
|
||||||
ExternalSystemNames: null, DatabaseConnectionNames: null,
|
ExternalSystemNames: null, DatabaseConnectionNames: null,
|
||||||
NotificationListNames: null, SmtpConfigurationNames: null,
|
NotificationListNames: null, SmtpConfigurationNames: null,
|
||||||
ApiKeyNames: null, ApiMethodNames: null,
|
ApiMethodNames: null,
|
||||||
IncludeDependencies: false, Passphrase: null,
|
IncludeDependencies: false, Passphrase: null,
|
||||||
SourceEnvironment: "test-env");
|
SourceEnvironment: "test-env");
|
||||||
|
|
||||||
@@ -1283,7 +1283,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
|||||||
SharedScriptNames: null,
|
SharedScriptNames: null,
|
||||||
ExternalSystemNames: null, DatabaseConnectionNames: null,
|
ExternalSystemNames: null, DatabaseConnectionNames: null,
|
||||||
NotificationListNames: null, SmtpConfigurationNames: null,
|
NotificationListNames: null, SmtpConfigurationNames: null,
|
||||||
ApiKeyNames: null, ApiMethodNames: null,
|
ApiMethodNames: null,
|
||||||
IncludeDependencies: false, Passphrase: null,
|
IncludeDependencies: false, Passphrase: null,
|
||||||
SourceEnvironment: "test-env");
|
SourceEnvironment: "test-env");
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ public sealed class CompositionImportTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ public sealed class ConflictResolutionTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ public sealed class BundleExporterTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: true);
|
IncludeDependencies: true);
|
||||||
|
|
||||||
@@ -222,7 +221,6 @@ public sealed class BundleExporterTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: true);
|
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.ConfigurationDatabase.Services;
|
||||||
using ZB.MOM.WW.ScadaBridge.Transport;
|
using ZB.MOM.WW.ScadaBridge.Transport;
|
||||||
using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
|
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
|
||||||
|
|
||||||
@@ -96,7 +97,6 @@ public sealed class BundleImporterApplyTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
||||||
@@ -747,4 +747,96 @@ public sealed class BundleImporterApplyTests : IDisposable
|
|||||||
}
|
}
|
||||||
Assert.Equal(1, result.Overwritten);
|
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>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
-1
@@ -181,7 +181,6 @@ public sealed class BundleImporterRollbackFailureTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ public sealed class RoundTripTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: notificationListIds,
|
NotificationListIds: notificationListIds,
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: true);
|
IncludeDependencies: true);
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ public sealed class SemanticValidatorImportTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ public sealed class ValidationFailureTests : IDisposable
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: false);
|
IncludeDependencies: false);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ public sealed class DependencyResolverTests
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: Array.Empty<int>(),
|
ApiMethodIds: Array.Empty<int>(),
|
||||||
IncludeDependencies: true);
|
IncludeDependencies: true);
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ public sealed class DependencyResolverTests
|
|||||||
DatabaseConnectionIds: Array.Empty<int>(),
|
DatabaseConnectionIds: Array.Empty<int>(),
|
||||||
NotificationListIds: Array.Empty<int>(),
|
NotificationListIds: Array.Empty<int>(),
|
||||||
SmtpConfigurationIds: Array.Empty<int>(),
|
SmtpConfigurationIds: Array.Empty<int>(),
|
||||||
ApiKeyIds: Array.Empty<int>(),
|
|
||||||
ApiMethodIds: ids,
|
ApiMethodIds: ids,
|
||||||
IncludeDependencies: true);
|
IncludeDependencies: true);
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ public sealed class BundleImporterLoadTests
|
|||||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||||
ApiKeys: Array.Empty<ApiKeyDto>(),
|
|
||||||
ApiMethods: Array.Empty<ApiMethodDto>());
|
ApiMethods: Array.Empty<ApiMethodDto>());
|
||||||
|
|
||||||
private static BundleContentDto SmallContent() => new(
|
private static BundleContentDto SmallContent() => new(
|
||||||
@@ -65,7 +64,6 @@ public sealed class BundleImporterLoadTests
|
|||||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||||
ApiKeys: Array.Empty<ApiKeyDto>(),
|
|
||||||
ApiMethods: Array.Empty<ApiMethodDto>());
|
ApiMethods: Array.Empty<ApiMethodDto>());
|
||||||
|
|
||||||
private sealed class TestTimeProvider : TimeProvider
|
private sealed class TestTimeProvider : TimeProvider
|
||||||
@@ -132,7 +130,7 @@ public sealed class BundleImporterLoadTests
|
|||||||
exportedBy: "alice",
|
exportedBy: "alice",
|
||||||
scadaBridgeVersion: "1.0.0",
|
scadaBridgeVersion: "1.0.0",
|
||||||
encryption: null,
|
encryption: null,
|
||||||
summary: new BundleSummary(content.Templates.Count, 0, 0, 0, 0, 0, 0, 0, 0),
|
summary: new BundleSummary(content.Templates.Count, 0, 0, 0, 0, 0, 0, 0),
|
||||||
contents: Array.Empty<ManifestContentEntry>(),
|
contents: Array.Empty<ManifestContentEntry>(),
|
||||||
contentBytes: contentBytes);
|
contentBytes: contentBytes);
|
||||||
return serializer.Pack(content, manifest, passphrase: null, encryptor: null);
|
return serializer.Pack(content, manifest, passphrase: null, encryptor: null);
|
||||||
@@ -154,7 +152,7 @@ public sealed class BundleImporterLoadTests
|
|||||||
exportedBy: "alice",
|
exportedBy: "alice",
|
||||||
scadaBridgeVersion: "1.0.0",
|
scadaBridgeVersion: "1.0.0",
|
||||||
encryption: seed,
|
encryption: seed,
|
||||||
summary: new BundleSummary(content.Templates.Count, 0, 0, 0, 0, 0, 0, 0, 0),
|
summary: new BundleSummary(content.Templates.Count, 0, 0, 0, 0, 0, 0, 0),
|
||||||
contents: Array.Empty<ManifestContentEntry>(),
|
contents: Array.Empty<ManifestContentEntry>(),
|
||||||
contentBytes: contentBytes);
|
contentBytes: contentBytes);
|
||||||
return serializer.Pack(content, manifest, passphrase, encryptor);
|
return serializer.Pack(content, manifest, passphrase, encryptor);
|
||||||
@@ -450,7 +448,7 @@ public sealed class BundleImporterLoadTests
|
|||||||
ScadaBridgeVersion: "1.0.0",
|
ScadaBridgeVersion: "1.0.0",
|
||||||
ContentHash: "sha256:" + Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant(),
|
ContentHash: "sha256:" + Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant(),
|
||||||
Encryption: null,
|
Encryption: null,
|
||||||
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0, 0),
|
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
Contents: Array.Empty<ManifestContentEntry>());
|
Contents: Array.Empty<ManifestContentEntry>());
|
||||||
|
|
||||||
var bundleStream = HandCraftZip(forwardManifest, contentBytes, encrypted: false);
|
var bundleStream = HandCraftZip(forwardManifest, contentBytes, encrypted: false);
|
||||||
@@ -473,7 +471,7 @@ public sealed class BundleImporterLoadTests
|
|||||||
exportedBy: "alice",
|
exportedBy: "alice",
|
||||||
scadaBridgeVersion: "1.0.0",
|
scadaBridgeVersion: "1.0.0",
|
||||||
encryption: null,
|
encryption: null,
|
||||||
summary: new BundleSummary(1, 0, 0, 0, 0, 0, 0, 0, 0),
|
summary: new BundleSummary(1, 0, 0, 0, 0, 0, 0, 0),
|
||||||
contents: Array.Empty<ManifestContentEntry>(),
|
contents: Array.Empty<ManifestContentEntry>(),
|
||||||
contentBytes: originalContentBytes);
|
contentBytes: originalContentBytes);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public sealed class BundleSessionStoreTests
|
|||||||
ScadaBridgeVersion: "1",
|
ScadaBridgeVersion: "1",
|
||||||
ContentHash: "0",
|
ContentHash: "0",
|
||||||
Encryption: null,
|
Encryption: null,
|
||||||
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0, 0),
|
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
Contents: Array.Empty<ManifestContentEntry>());
|
Contents: Array.Empty<ManifestContentEntry>());
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public sealed class BundleSerializerTests
|
|||||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||||
ApiKeys: Array.Empty<ApiKeyDto>(),
|
|
||||||
ApiMethods: Array.Empty<ApiMethodDto>());
|
ApiMethods: Array.Empty<ApiMethodDto>());
|
||||||
|
|
||||||
private static BundleManifest BuildManifestFor(byte[] contentBytes, EncryptionMetadata? encryption = null) =>
|
private static BundleManifest BuildManifestFor(byte[] contentBytes, EncryptionMetadata? encryption = null) =>
|
||||||
@@ -30,7 +29,7 @@ public sealed class BundleSerializerTests
|
|||||||
exportedBy: "tester",
|
exportedBy: "tester",
|
||||||
scadaBridgeVersion: "1.0.0",
|
scadaBridgeVersion: "1.0.0",
|
||||||
encryption: encryption,
|
encryption: encryption,
|
||||||
summary: new BundleSummary(0, 1, 1, 0, 0, 0, 0, 0, 0),
|
summary: new BundleSummary(0, 1, 1, 0, 0, 0, 0, 0),
|
||||||
contents: Array.Empty<ManifestContentEntry>(),
|
contents: Array.Empty<ManifestContentEntry>(),
|
||||||
contentBytes: contentBytes);
|
contentBytes: contentBytes);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ public sealed class EntitySerializerTests
|
|||||||
DatabaseConnections: Array.Empty<DatabaseConnectionDefinition>(),
|
DatabaseConnections: Array.Empty<DatabaseConnectionDefinition>(),
|
||||||
NotificationLists: Array.Empty<NotificationList>(),
|
NotificationLists: Array.Empty<NotificationList>(),
|
||||||
SmtpConfigurations: Array.Empty<SmtpConfiguration>(),
|
SmtpConfigurations: Array.Empty<SmtpConfiguration>(),
|
||||||
ApiKeys: Array.Empty<ApiKey>(),
|
|
||||||
ApiMethods: Array.Empty<ApiMethod>());
|
ApiMethods: Array.Empty<ApiMethod>());
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -217,7 +216,6 @@ public sealed class EntitySerializerTests
|
|||||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||||
ApiKeys: Array.Empty<ApiKeyDto>(),
|
|
||||||
ApiMethods: Array.Empty<ApiMethodDto>());
|
ApiMethods: Array.Empty<ApiMethodDto>());
|
||||||
|
|
||||||
var aggregate = new EntitySerializer().FromBundleContent(dto);
|
var aggregate = new EntitySerializer().FromBundleContent(dto);
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Serialization;
|
|||||||
|
|
||||||
public sealed class ManifestBuilderTests
|
public sealed class ManifestBuilderTests
|
||||||
{
|
{
|
||||||
private static BundleSummary EmptySummary => new(0, 0, 0, 0, 0, 0, 0, 0, 0);
|
private static BundleSummary EmptySummary => new(0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
private static IReadOnlyList<ManifestContentEntry> NoContents => Array.Empty<ManifestContentEntry>();
|
private static IReadOnlyList<ManifestContentEntry> NoContents => Array.Empty<ManifestContentEntry>();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_populates_summary_from_contents()
|
public void Build_populates_summary_from_contents()
|
||||||
{
|
{
|
||||||
var sut = new ManifestBuilder();
|
var sut = new ManifestBuilder();
|
||||||
var summary = new BundleSummary(2, 1, 0, 0, 0, 0, 0, 0, 0);
|
var summary = new BundleSummary(2, 1, 0, 0, 0, 0, 0, 0);
|
||||||
var contents = new[]
|
var contents = new[]
|
||||||
{
|
{
|
||||||
new ManifestContentEntry("Template", "T1", 1, Array.Empty<string>()),
|
new ManifestContentEntry("Template", "T1", 1, Array.Empty<string>()),
|
||||||
|
|||||||
Reference in New Issue
Block a user