Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added MongoPackRunApprovalStore for managing approval states with MongoDB. - Introduced MongoPackRunArtifactUploader for uploading and storing artifacts. - Created MongoPackRunLogStore to handle logging of pack run events. - Developed MongoPackRunStateStore for persisting and retrieving pack run states. - Implemented unit tests for MongoDB stores to ensure correct functionality. - Added MongoTaskRunnerTestContext for setting up MongoDB test environment. - Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
348 lines
11 KiB
C#
348 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Scanner.Surface.Secrets;
|
|
|
|
public sealed record RegistryAccessSecret(
|
|
IReadOnlyList<RegistryCredential> Entries,
|
|
string? DefaultRegistry);
|
|
|
|
public sealed record RegistryCredential(
|
|
string Registry,
|
|
string? Username,
|
|
string? Password,
|
|
string? IdentityToken,
|
|
string? RegistryToken,
|
|
string? RefreshToken,
|
|
DateTimeOffset? ExpiresAt,
|
|
IReadOnlyCollection<string> Scopes,
|
|
bool? AllowInsecureTls,
|
|
IReadOnlyDictionary<string, string> Headers,
|
|
string? Email);
|
|
|
|
public static partial class SurfaceSecretParser
|
|
{
|
|
public static RegistryAccessSecret ParseRegistryAccessSecret(SurfaceSecretHandle handle)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(handle);
|
|
|
|
var entries = new List<RegistryCredential>();
|
|
string? defaultRegistry = null;
|
|
|
|
var payload = handle.AsBytes();
|
|
if (!payload.IsEmpty)
|
|
{
|
|
var jsonText = DecodeUtf8(payload);
|
|
using var document = JsonDocument.Parse(jsonText);
|
|
var root = document.RootElement;
|
|
|
|
defaultRegistry = GetString(root, "defaultRegistry") ?? GetMetadataValue(handle.Metadata, "defaultRegistry");
|
|
|
|
if (TryParseRegistryEntries(root, handle.Metadata, entries) ||
|
|
TryParseAuthsObject(root, handle.Metadata, entries))
|
|
{
|
|
// entries already populated
|
|
}
|
|
else if (root.ValueKind == JsonValueKind.Object && root.GetRawText().Length > 2) // not empty object
|
|
{
|
|
entries.Add(ParseRegistryEntry(root, handle.Metadata, fallbackRegistry: null));
|
|
}
|
|
}
|
|
|
|
if (entries.Count == 0 && TryCreateRegistryEntryFromMetadata(handle.Metadata, out var metadataEntry))
|
|
{
|
|
entries.Add(metadataEntry);
|
|
}
|
|
|
|
if (entries.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("Registry secret payload does not contain credentials.");
|
|
}
|
|
|
|
defaultRegistry ??= GetMetadataValue(handle.Metadata, "defaultRegistry")
|
|
?? entries[0].Registry;
|
|
|
|
return new RegistryAccessSecret(
|
|
new ReadOnlyCollection<RegistryCredential>(entries),
|
|
string.IsNullOrWhiteSpace(defaultRegistry) ? entries[0].Registry : defaultRegistry.Trim());
|
|
}
|
|
|
|
private static bool TryParseRegistryEntries(
|
|
JsonElement root,
|
|
IReadOnlyDictionary<string, string> metadata,
|
|
ICollection<RegistryCredential> entries)
|
|
{
|
|
if (!TryGetPropertyIgnoreCase(root, "entries", out var entriesElement) ||
|
|
entriesElement.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var entryElement in entriesElement.EnumerateArray())
|
|
{
|
|
if (entryElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
entries.Add(ParseRegistryEntry(entryElement, metadata, fallbackRegistry: null));
|
|
}
|
|
|
|
return entries.Count > 0;
|
|
}
|
|
|
|
private static bool TryParseAuthsObject(
|
|
JsonElement root,
|
|
IReadOnlyDictionary<string, string> metadata,
|
|
ICollection<RegistryCredential> entries)
|
|
{
|
|
if (!TryGetPropertyIgnoreCase(root, "auths", out var authsElement) ||
|
|
authsElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var property in authsElement.EnumerateObject())
|
|
{
|
|
if (property.Value.ValueKind != JsonValueKind.Object)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
entries.Add(ParseRegistryEntry(property.Value, metadata, property.Name));
|
|
}
|
|
|
|
return entries.Count > 0;
|
|
}
|
|
|
|
private static RegistryCredential ParseRegistryEntry(
|
|
JsonElement element,
|
|
IReadOnlyDictionary<string, string> metadata,
|
|
string? fallbackRegistry)
|
|
{
|
|
var registry = GetString(element, "registry")
|
|
?? GetString(element, "server")
|
|
?? fallbackRegistry
|
|
?? GetMetadataValue(metadata, "registry")
|
|
?? throw new InvalidOperationException("Registry credential is missing a registry identifier.");
|
|
|
|
registry = registry.Trim();
|
|
|
|
var username = GetString(element, "username") ?? GetString(element, "user");
|
|
var password = GetString(element, "password") ?? GetString(element, "pass");
|
|
var token = GetString(element, "token") ?? GetString(element, "registryToken");
|
|
var identityToken = GetString(element, "identityToken") ?? GetString(element, "identitytoken");
|
|
var refreshToken = GetString(element, "refreshToken");
|
|
var email = GetString(element, "email");
|
|
var allowInsecure = GetBoolean(element, "allowInsecureTls");
|
|
|
|
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
PopulateHeaders(element, headers);
|
|
PopulateMetadataHeaders(metadata, headers);
|
|
|
|
var scopes = new List<string>();
|
|
PopulateScopes(element, scopes);
|
|
PopulateMetadataScopes(metadata, scopes);
|
|
|
|
var expiresAt = ParseDateTime(element, "expiresAt");
|
|
|
|
var auth = GetString(element, "auth");
|
|
if (!string.IsNullOrWhiteSpace(auth))
|
|
{
|
|
TryApplyBasicAuth(auth, ref username, ref password);
|
|
}
|
|
|
|
username ??= GetMetadataValue(metadata, "username");
|
|
password ??= GetMetadataValue(metadata, "password");
|
|
token ??= GetMetadataValue(metadata, "token") ?? GetMetadataValue(metadata, "registryToken");
|
|
identityToken ??= GetMetadataValue(metadata, "identityToken");
|
|
refreshToken ??= GetMetadataValue(metadata, "refreshToken");
|
|
email ??= GetMetadataValue(metadata, "email");
|
|
|
|
return new RegistryCredential(
|
|
registry,
|
|
username?.Trim(),
|
|
password,
|
|
identityToken,
|
|
token,
|
|
refreshToken,
|
|
expiresAt,
|
|
scopes.Count == 0 ? Array.Empty<string>() : new ReadOnlyCollection<string>(scopes),
|
|
allowInsecure,
|
|
new ReadOnlyDictionary<string, string>(headers),
|
|
email);
|
|
}
|
|
|
|
private static bool TryCreateRegistryEntryFromMetadata(
|
|
IReadOnlyDictionary<string, string> metadata,
|
|
out RegistryCredential entry)
|
|
{
|
|
var registry = GetMetadataValue(metadata, "registry");
|
|
var username = GetMetadataValue(metadata, "username");
|
|
var password = GetMetadataValue(metadata, "password");
|
|
var identityToken = GetMetadataValue(metadata, "identityToken");
|
|
var token = GetMetadataValue(metadata, "token") ?? GetMetadataValue(metadata, "registryToken");
|
|
|
|
if (string.IsNullOrWhiteSpace(registry) &&
|
|
string.IsNullOrWhiteSpace(username) &&
|
|
string.IsNullOrWhiteSpace(password) &&
|
|
string.IsNullOrWhiteSpace(identityToken) &&
|
|
string.IsNullOrWhiteSpace(token))
|
|
{
|
|
entry = null!;
|
|
return false;
|
|
}
|
|
|
|
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
PopulateMetadataHeaders(metadata, headers);
|
|
|
|
var scopes = new List<string>();
|
|
PopulateMetadataScopes(metadata, scopes);
|
|
|
|
entry = new RegistryCredential(
|
|
registry?.Trim() ?? "registry.local",
|
|
username?.Trim(),
|
|
password,
|
|
identityToken,
|
|
token,
|
|
GetMetadataValue(metadata, "refreshToken"),
|
|
ParseDateTime(metadata, "expiresAt"),
|
|
scopes.Count == 0 ? Array.Empty<string>() : new ReadOnlyCollection<string>(scopes),
|
|
ParseBoolean(metadata, "allowInsecureTls"),
|
|
new ReadOnlyDictionary<string, string>(headers),
|
|
GetMetadataValue(metadata, "email"));
|
|
return true;
|
|
}
|
|
|
|
private static void PopulateScopes(JsonElement element, ICollection<string> scopes)
|
|
{
|
|
if (!TryGetPropertyIgnoreCase(element, "scopes", out var scopesElement))
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (scopesElement.ValueKind)
|
|
{
|
|
case JsonValueKind.Array:
|
|
foreach (var scope in scopesElement.EnumerateArray())
|
|
{
|
|
if (scope.ValueKind == JsonValueKind.String)
|
|
{
|
|
var value = scope.GetString();
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
{
|
|
scopes.Add(value.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
case JsonValueKind.String:
|
|
var text = scopesElement.GetString();
|
|
if (!string.IsNullOrWhiteSpace(text))
|
|
{
|
|
foreach (var part in text.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
scopes.Add(part.Trim());
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void PopulateMetadataScopes(IReadOnlyDictionary<string, string> metadata, ICollection<string> scopes)
|
|
{
|
|
foreach (var (key, value) in metadata)
|
|
{
|
|
if (!key.StartsWith("scope", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
scopes.Add(value.Trim());
|
|
}
|
|
}
|
|
|
|
private static void TryApplyBasicAuth(string auth, ref string? username, ref string? password)
|
|
{
|
|
try
|
|
{
|
|
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(auth));
|
|
var separator = decoded.IndexOf(':');
|
|
if (separator >= 0)
|
|
{
|
|
username ??= decoded[..separator];
|
|
password ??= decoded[(separator + 1)..];
|
|
}
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
// ignore malformed auth; caller may still have explicit username/password fields
|
|
}
|
|
}
|
|
|
|
private static DateTimeOffset? ParseDateTime(JsonElement element, string propertyName)
|
|
{
|
|
if (!TryGetPropertyIgnoreCase(element, propertyName, out var value) ||
|
|
value.ValueKind != JsonValueKind.String)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var text = value.GetString();
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (DateTimeOffset.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static DateTimeOffset? ParseDateTime(IReadOnlyDictionary<string, string> metadata, string key)
|
|
{
|
|
var value = GetMetadataValue(metadata, key);
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool? ParseBoolean(IReadOnlyDictionary<string, string> metadata, string key)
|
|
{
|
|
var value = GetMetadataValue(metadata, key);
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (bool.TryParse(value, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|