Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
using System.Buffers;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
using StellaOps.Zastava.Core.Hashing;
|
||||
using StellaOps.Zastava.Core.Serialization;
|
||||
|
||||
namespace StellaOps.Zastava.Webhook.Admission;
|
||||
|
||||
internal sealed class AdmissionResponseBuilder
|
||||
{
|
||||
public (AdmissionDecisionEnvelope Envelope, AdmissionReviewResponseDto Response) Build(
|
||||
AdmissionRequestContext context,
|
||||
RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
var decision = BuildDecision(context, evaluation);
|
||||
var envelope = AdmissionDecisionEnvelope.Create(decision, ZastavaContractVersions.AdmissionDecision);
|
||||
var auditAnnotations = CreateAuditAnnotations(envelope, evaluation);
|
||||
|
||||
var warnings = BuildWarnings(evaluation);
|
||||
var allowed = evaluation.Decisions.All(static d => d.Allowed);
|
||||
var status = allowed
|
||||
? null
|
||||
: new AdmissionReviewStatus
|
||||
{
|
||||
Code = 403,
|
||||
Message = BuildFailureMessage(evaluation)
|
||||
};
|
||||
|
||||
var response = new AdmissionReviewResponseDto
|
||||
{
|
||||
ApiVersion = context.ApiVersion,
|
||||
Kind = context.Kind,
|
||||
Response = new AdmissionReviewResponsePayload
|
||||
{
|
||||
Uid = context.Uid,
|
||||
Allowed = allowed,
|
||||
Status = status,
|
||||
Warnings = warnings,
|
||||
AuditAnnotations = auditAnnotations
|
||||
}
|
||||
};
|
||||
|
||||
return (envelope, response);
|
||||
}
|
||||
|
||||
private static AdmissionDecision BuildDecision(AdmissionRequestContext context, RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
var images = new List<AdmissionImageVerdict>(evaluation.Decisions.Count);
|
||||
for (var i = 0; i < evaluation.Decisions.Count; i++)
|
||||
{
|
||||
var decision = evaluation.Decisions[i];
|
||||
var container = context.Containers[Math.Min(i, context.Containers.Count - 1)];
|
||||
|
||||
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["image"] = decision.OriginalImage
|
||||
};
|
||||
|
||||
if (!string.Equals(container.Image, container.Name, StringComparison.Ordinal))
|
||||
{
|
||||
metadata["container"] = container.Name;
|
||||
}
|
||||
|
||||
if (decision.FromCache)
|
||||
{
|
||||
metadata["cache"] = "hit";
|
||||
}
|
||||
|
||||
var resolved = decision.ResolvedDigest ?? decision.OriginalImage;
|
||||
|
||||
images.Add(new AdmissionImageVerdict
|
||||
{
|
||||
Name = container.Name,
|
||||
Resolved = resolved,
|
||||
Signed = decision.Policy?.Signed ?? false,
|
||||
HasSbomReferrers = decision.Policy?.HasSbom ?? false,
|
||||
PolicyVerdict = decision.Verdict,
|
||||
Reasons = decision.Reasons,
|
||||
Rekor = decision.Policy?.Rekor,
|
||||
Metadata = metadata
|
||||
});
|
||||
}
|
||||
|
||||
return new AdmissionDecision
|
||||
{
|
||||
AdmissionId = context.Uid,
|
||||
Namespace = context.Namespace,
|
||||
PodSpecDigest = ComputePodSpecDigest(context.PodSpec),
|
||||
Images = images,
|
||||
Decision = evaluation.Decisions.All(static d => d.Allowed)
|
||||
? AdmissionDecisionOutcome.Allow
|
||||
: AdmissionDecisionOutcome.Deny,
|
||||
TtlSeconds = Math.Max(0, evaluation.TtlSeconds),
|
||||
Annotations = BuildAnnotations(evaluation)
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string>? BuildAnnotations(RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
if (!evaluation.BackendFailed && !evaluation.FailOpenApplied && evaluation.FailureReason is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var annotations = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (evaluation.BackendFailed)
|
||||
{
|
||||
annotations["zastava.backend.failed"] = "true";
|
||||
}
|
||||
|
||||
if (evaluation.FailOpenApplied)
|
||||
{
|
||||
annotations["zastava.failOpen"] = "true";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(evaluation.FailureReason))
|
||||
{
|
||||
annotations["zastava.failureReason"] = evaluation.FailureReason!;
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> CreateAuditAnnotations(AdmissionDecisionEnvelope envelope, RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["zastava.stellaops/admission"] = ZastavaCanonicalJsonSerializer.Serialize(envelope)
|
||||
};
|
||||
|
||||
if (evaluation.FailOpenApplied)
|
||||
{
|
||||
annotations["zastava.stellaops/failOpen"] = "true";
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string>? BuildWarnings(RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (evaluation.FailOpenApplied)
|
||||
{
|
||||
warnings.Add("zastava.fail_open.applied");
|
||||
}
|
||||
|
||||
foreach (var decision in evaluation.Decisions)
|
||||
{
|
||||
if (decision.Verdict == PolicyVerdict.Warn)
|
||||
{
|
||||
warnings.Add($"policy.warn:{decision.OriginalImage}");
|
||||
}
|
||||
}
|
||||
|
||||
return warnings.Count == 0 ? null : warnings;
|
||||
}
|
||||
|
||||
private static string BuildFailureMessage(RuntimeAdmissionEvaluation evaluation)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(evaluation.FailureReason))
|
||||
{
|
||||
return evaluation.FailureReason!;
|
||||
}
|
||||
|
||||
var denied = evaluation.Decisions
|
||||
.Where(static d => !d.Allowed)
|
||||
.SelectMany(static d => d.Reasons)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
return denied.Length > 0
|
||||
? string.Join(", ", denied)
|
||||
: "admission.denied";
|
||||
}
|
||||
|
||||
private static string ComputePodSpecDigest(JsonElement podSpec)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = false
|
||||
}))
|
||||
{
|
||||
WriteCanonical(podSpec, writer);
|
||||
}
|
||||
|
||||
return ZastavaHashing.ComputeMultihash(buffer.WrittenSpan);
|
||||
}
|
||||
|
||||
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
writer.WriteStartObject();
|
||||
foreach (var property in element.EnumerateObject().OrderBy(static p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
writer.WritePropertyName(property.Name);
|
||||
WriteCanonical(property.Value, writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
WriteCanonical(item, writer);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
break;
|
||||
default:
|
||||
element.WriteTo(writer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user