feat(artifacts): additive Secrets list + SecretsApplied on the artifact deploy contract (proto field 9)
This commit is contained in:
@@ -5,4 +5,5 @@ public record ArtifactDeploymentResponse(
|
||||
string SiteId,
|
||||
bool Success,
|
||||
string? ErrorMessage,
|
||||
DateTimeOffset Timestamp);
|
||||
DateTimeOffset Timestamp,
|
||||
int? SecretsApplied = null);
|
||||
|
||||
@@ -8,4 +8,5 @@ public record DeployArtifactsCommand(
|
||||
IReadOnlyList<NotificationListArtifact>? NotificationLists,
|
||||
IReadOnlyList<DataConnectionArtifact>? DataConnections,
|
||||
IReadOnlyList<SmtpConfigurationArtifact>? SmtpConfigurations,
|
||||
DateTimeOffset Timestamp);
|
||||
DateTimeOffset Timestamp,
|
||||
IReadOnlyList<SecretArtifact>? Secrets = null);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
||||
|
||||
/// <summary>
|
||||
/// One secret shipped to a site with its artifacts: the ZB.MOM.WW.Secrets StoredSecret row
|
||||
/// VERBATIM — ciphertext under the fleet's shared KEK, never plaintext. The site applies it
|
||||
/// with ISecretStore.ApplyReplicatedAsync. Design: docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md §3.3.
|
||||
/// </summary>
|
||||
public sealed record SecretArtifact(
|
||||
string Name,
|
||||
string ContentType,
|
||||
byte[] Ciphertext,
|
||||
byte[] Nonce,
|
||||
byte[] Tag,
|
||||
byte[] WrappedDek,
|
||||
byte[] WrapNonce,
|
||||
byte[] WrapTag,
|
||||
string KekId,
|
||||
long Revision,
|
||||
DateTimeOffset CreatedUtc,
|
||||
DateTimeOffset UpdatedUtc,
|
||||
string? Description = null);
|
||||
@@ -1,3 +1,4 @@
|
||||
using Google.Protobuf;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
@@ -444,6 +445,13 @@ public static class SiteCommandDtoMapper
|
||||
dto.SmtpConfigurations = list;
|
||||
}
|
||||
|
||||
if (command.Secrets is not null)
|
||||
{
|
||||
var list = new SecretArtifactListDto();
|
||||
list.Items.AddRange(command.Secrets.Select(ToProto));
|
||||
dto.Secrets = list;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -462,7 +470,56 @@ public static class SiteCommandDtoMapper
|
||||
dto.NotificationLists is null ? null : dto.NotificationLists.Items.Select(FromProto).ToList(),
|
||||
dto.DataConnections is null ? null : dto.DataConnections.Items.Select(FromProto).ToList(),
|
||||
dto.SmtpConfigurations is null ? null : dto.SmtpConfigurations.Items.Select(FromProto).ToList(),
|
||||
Dto(dto.Timestamp));
|
||||
Dto(dto.Timestamp),
|
||||
dto.Secrets is null ? null : dto.Secrets.Items.Select(FromProto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>Projects a <see cref="SecretArtifact"/> onto the wire.</summary>
|
||||
/// <param name="artifact">The artifact to project.</param>
|
||||
/// <returns>The wire message.</returns>
|
||||
public static SecretArtifactDto ToProto(SecretArtifact artifact)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(artifact);
|
||||
|
||||
return new SecretArtifactDto
|
||||
{
|
||||
Name = artifact.Name,
|
||||
ContentType = artifact.ContentType,
|
||||
Ciphertext = ByteString.CopyFrom(artifact.Ciphertext),
|
||||
Nonce = ByteString.CopyFrom(artifact.Nonce),
|
||||
Tag = ByteString.CopyFrom(artifact.Tag),
|
||||
WrappedDek = ByteString.CopyFrom(artifact.WrappedDek),
|
||||
WrapNonce = ByteString.CopyFrom(artifact.WrapNonce),
|
||||
WrapTag = ByteString.CopyFrom(artifact.WrapTag),
|
||||
KekId = artifact.KekId,
|
||||
Revision = artifact.Revision,
|
||||
CreatedUtc = Ts(artifact.CreatedUtc),
|
||||
UpdatedUtc = Ts(artifact.UpdatedUtc),
|
||||
Description = artifact.Description
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Rehydrates a <see cref="SecretArtifact"/> from the wire.</summary>
|
||||
/// <param name="dto">The wire message to decode.</param>
|
||||
/// <returns>The decoded artifact.</returns>
|
||||
public static SecretArtifact FromProto(SecretArtifactDto dto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
|
||||
return new SecretArtifact(
|
||||
dto.Name,
|
||||
dto.ContentType,
|
||||
dto.Ciphertext.ToByteArray(),
|
||||
dto.Nonce.ToByteArray(),
|
||||
dto.Tag.ToByteArray(),
|
||||
dto.WrappedDek.ToByteArray(),
|
||||
dto.WrapNonce.ToByteArray(),
|
||||
dto.WrapTag.ToByteArray(),
|
||||
dto.KekId,
|
||||
dto.Revision,
|
||||
Dto(dto.CreatedUtc),
|
||||
Dto(dto.UpdatedUtc),
|
||||
dto.Description);
|
||||
}
|
||||
|
||||
/// <summary>Projects a <see cref="SharedScriptArtifact"/> onto the wire.</summary>
|
||||
@@ -756,7 +813,7 @@ public static class SiteCommandDtoMapper
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(response);
|
||||
|
||||
return new ArtifactDeploymentResponseDto
|
||||
var dto = new ArtifactDeploymentResponseDto
|
||||
{
|
||||
DeploymentId = response.DeploymentId,
|
||||
SiteId = response.SiteId,
|
||||
@@ -764,6 +821,13 @@ public static class SiteCommandDtoMapper
|
||||
ErrorMessage = OrEmpty(response.ErrorMessage),
|
||||
Timestamp = Ts(response.Timestamp)
|
||||
};
|
||||
|
||||
if (response.SecretsApplied.HasValue)
|
||||
{
|
||||
dto.SecretsApplied = response.SecretsApplied.Value;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>Rehydrates an <see cref="ArtifactDeploymentResponse"/> from the wire.</summary>
|
||||
@@ -774,7 +838,8 @@ public static class SiteCommandDtoMapper
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
|
||||
return new ArtifactDeploymentResponse(
|
||||
dto.DeploymentId, dto.SiteId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp));
|
||||
dto.DeploymentId, dto.SiteId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp),
|
||||
dto.SecretsApplied);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -270,6 +270,25 @@ message SmtpConfigurationArtifactDto {
|
||||
// own message makes the null/empty distinction a message-presence question,
|
||||
// which proto3 does model — the same silent-data-loss class the transport
|
||||
// round-trip guard caught in PLAN-05 T8.
|
||||
// One StoredSecret row, ciphertext verbatim under the fleet's shared KEK — never
|
||||
// plaintext on the wire. Site secrets design §3.3.
|
||||
message SecretArtifactDto {
|
||||
string name = 1;
|
||||
string content_type = 2;
|
||||
bytes ciphertext = 3;
|
||||
bytes nonce = 4;
|
||||
bytes tag = 5;
|
||||
bytes wrapped_dek = 6;
|
||||
bytes wrap_nonce = 7;
|
||||
bytes wrap_tag = 8;
|
||||
string kek_id = 9;
|
||||
int64 revision = 10;
|
||||
google.protobuf.Timestamp created_utc = 11;
|
||||
google.protobuf.Timestamp updated_utc = 12;
|
||||
google.protobuf.StringValue description = 13; // absent => null
|
||||
}
|
||||
message SecretArtifactListDto { repeated SecretArtifactDto items = 1; }
|
||||
|
||||
message SharedScriptArtifactListDto { repeated SharedScriptArtifactDto items = 1; }
|
||||
message ExternalSystemArtifactListDto { repeated ExternalSystemArtifactDto items = 1; }
|
||||
message DatabaseConnectionArtifactListDto { repeated DatabaseConnectionArtifactDto items = 1; }
|
||||
@@ -286,6 +305,7 @@ message DeployArtifactsCommandDto {
|
||||
DataConnectionArtifactListDto data_connections = 6; // absent => null
|
||||
SmtpConfigurationArtifactListDto smtp_configurations = 7; // absent => null
|
||||
google.protobuf.Timestamp timestamp = 8;
|
||||
SecretArtifactListDto secrets = 9; // absent => null
|
||||
}
|
||||
|
||||
message DeploymentStatusResponseDto {
|
||||
@@ -319,6 +339,7 @@ message ArtifactDeploymentResponseDto {
|
||||
bool success = 3;
|
||||
string error_message = 4; // empty string represents null
|
||||
google.protobuf.Timestamp timestamp = 5;
|
||||
google.protobuf.Int32Value secrets_applied = 6; // absent => null
|
||||
}
|
||||
|
||||
message LifecycleRequest {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user