< Summary

Information
Class: AsiBackbone.Core.CapabilityGrants.CapabilityGrant
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityGrants/CapabilityGrant.cs
Line coverage
100%
Covered lines: 101
Uncovered lines: 0
Coverable lines: 101
Total lines: 288
Line coverage: 100%
Branch coverage
100%
Covered branches: 32
Total branches: 32
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%1010100%
get_HasAcknowledgmentReference()100%11100%
get_HasHandshakeReference()100%11100%
get_HasMetadata()100%11100%
Create(...)100%44100%
NormalizeScopes(...)100%22100%
NormalizeMetadata(...)100%1414100%
NormalizeOptional(...)100%22100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityGrants/CapabilityGrant.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using AsiBackbone.Core.Serialization;
 3
 4namespace AsiBackbone.Core.CapabilityGrants;
 5
 6/// <summary>
 7/// Represents a provider-neutral, short-lived capability grant for follow-on governed execution.
 8/// </summary>
 9/// <remarks>
 10/// The grant is a metadata model, not a bearer-token format. Hosts decide how this grant is serialized,
 11/// transported, protected, and bound to their authentication and authorization systems.
 12/// </remarks>
 13public sealed class CapabilityGrant
 14{
 115    private static readonly ReadOnlyCollection<string> EmptyScopes =
 116        Array.AsReadOnly(Array.Empty<string>());
 17
 118    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 119        new ReadOnlyDictionary<string, string>(
 120            new Dictionary<string, string>(StringComparer.Ordinal));
 21
 15922    private CapabilityGrant(
 15923        string tokenId,
 15924        string issuer,
 15925        string audience,
 15926        IReadOnlyList<string> scopes,
 15927        DateTimeOffset issuedUtc,
 15928        DateTimeOffset? notBeforeUtc,
 15929        DateTimeOffset expiresUtc,
 15930        string? subjectId,
 15931        string? operationName,
 15932        string? policyVersion,
 15933        string? policyHash,
 15934        string? acknowledgmentId,
 15935        string? handshakeId,
 15936        string? gatewayBinding,
 15937        string? resourceBinding,
 15938        IReadOnlyDictionary<string, string> metadata,
 15939        int? maxUseCount,
 15940        string? schemaVersion)
 41    {
 15942        ArgumentException.ThrowIfNullOrWhiteSpace(tokenId);
 15943        ArgumentException.ThrowIfNullOrWhiteSpace(issuer);
 15944        ArgumentException.ThrowIfNullOrWhiteSpace(audience);
 15945        ArgumentNullException.ThrowIfNull(scopes);
 46
 15947        if (scopes.Count == 0)
 48        {
 149            throw new ArgumentException("At least one capability scope is required.", nameof(scopes));
 50        }
 51
 15852        DateTimeOffset normalizedIssuedUtc = issuedUtc.ToUniversalTime();
 15853        DateTimeOffset? normalizedNotBeforeUtc = notBeforeUtc?.ToUniversalTime();
 15854        DateTimeOffset normalizedExpiresUtc = expiresUtc.ToUniversalTime();
 55
 15856        if (normalizedNotBeforeUtc.HasValue && normalizedNotBeforeUtc.Value > normalizedExpiresUtc)
 57        {
 158            throw new ArgumentOutOfRangeException(nameof(notBeforeUtc), notBeforeUtc, "Not-before time must be earlier t
 59        }
 60
 15761        if (normalizedIssuedUtc > normalizedExpiresUtc)
 62        {
 163            throw new ArgumentOutOfRangeException(nameof(expiresUtc), expiresUtc, "Expiration time must be later than or
 64        }
 65
 15666        TokenId = tokenId.Trim();
 15667        Issuer = issuer.Trim();
 15668        Audience = audience.Trim();
 15669        Scopes = scopes;
 15670        IssuedUtc = normalizedIssuedUtc;
 15671        NotBeforeUtc = normalizedNotBeforeUtc;
 15672        ExpiresUtc = normalizedExpiresUtc;
 15673        SubjectId = NormalizeOptional(subjectId);
 15674        OperationName = NormalizeOptional(operationName);
 15675        PolicyVersion = NormalizeOptional(policyVersion);
 15676        PolicyHash = NormalizeOptional(policyHash);
 15677        AcknowledgmentId = NormalizeOptional(acknowledgmentId);
 15678        HandshakeId = NormalizeOptional(handshakeId);
 15679        GatewayBinding = NormalizeOptional(gatewayBinding);
 15680        ResourceBinding = NormalizeOptional(resourceBinding);
 15681        Metadata = metadata;
 15682        MaxUseCount = maxUseCount;
 15683        SchemaVersion = GovernanceSchemaVersions.Normalize(schemaVersion);
 15684    }
 85
 86    /// <summary>
 87    /// Gets the maximum number of times the issuer permits this grant to be used, when the issuer bound one.
 88    /// </summary>
 89    /// <remarks>
 90    /// A use limit supplied only at the validation call site is a local policy choice that the issuer never authorized 
 91    /// that nothing in the signature covers. When this value is present it is part of the signed payload, so a relying
 92    /// party cannot widen it. Validation uses the narrower of this value and any limit the caller supplies.
 93    /// </remarks>
 94    public int? MaxUseCount { get; }
 95
 96    /// <summary>
 97    /// Gets the stable grant identifier used for validation and replay checks.
 98    /// </summary>
 99    public string TokenId { get; }
 100
 101    /// <summary>
 102    /// Gets the issuer that created the grant.
 103    /// </summary>
 104    public string Issuer { get; }
 105
 106    /// <summary>
 107    /// Gets the intended audience for the grant.
 108    /// </summary>
 109    public string Audience { get; }
 110
 111    /// <summary>
 112    /// Gets the least-privilege scopes carried by the grant.
 113    /// </summary>
 114    public IReadOnlyList<string> Scopes { get; }
 115
 116    /// <summary>
 117    /// Gets the UTC timestamp when the grant was issued.
 118    /// </summary>
 119    public DateTimeOffset IssuedUtc { get; }
 120
 121    /// <summary>
 122    /// Gets the UTC timestamp before which the grant is not valid.
 123    /// </summary>
 124    public DateTimeOffset? NotBeforeUtc { get; }
 125
 126    /// <summary>
 127    /// Gets the UTC timestamp when the grant expires.
 128    /// </summary>
 129    public DateTimeOffset ExpiresUtc { get; }
 130
 131    /// <summary>
 132    /// Gets the host-defined subject identifier, when supplied.
 133    /// </summary>
 134    public string? SubjectId { get; }
 135
 136    /// <summary>
 137    /// Gets the operation name or action family the grant is intended to authorize.
 138    /// </summary>
 139    public string? OperationName { get; }
 140
 141    /// <summary>
 142    /// Gets the policy version bound to the grant, when supplied.
 143    /// </summary>
 144    public string? PolicyVersion { get; }
 145
 146    /// <summary>
 147    /// Gets the policy hash bound to the grant, when supplied.
 148    /// </summary>
 149    public string? PolicyHash { get; }
 150
 151    /// <summary>
 152    /// Gets the acknowledgment identifier bound to the grant, when supplied.
 153    /// </summary>
 154    public string? AcknowledgmentId { get; }
 155
 156    /// <summary>
 157    /// Gets the handshake identifier bound to the grant, when supplied.
 158    /// </summary>
 159    public string? HandshakeId { get; }
 160
 161    /// <summary>
 162    /// Gets the optional gateway binding used to limit execution context.
 163    /// </summary>
 164    public string? GatewayBinding { get; }
 165
 166    /// <summary>
 167    /// Gets the optional resource binding used to limit the target resource.
 168    /// </summary>
 169    public string? ResourceBinding { get; }
 170
 171    /// <summary>
 172    /// Gets the canonical schema version for this grant.
 173    /// </summary>
 174    public string SchemaVersion { get; }
 175
 176    /// <summary>
 177    /// Gets provider-neutral metadata carried with the grant.
 178    /// </summary>
 179    public IReadOnlyDictionary<string, string> Metadata { get; }
 180
 181    /// <summary>
 182    /// Gets a value indicating whether an acknowledgment reference is present.
 183    /// </summary>
 4184    public bool HasAcknowledgmentReference => AcknowledgmentId is not null;
 185
 186    /// <summary>
 187    /// Gets a value indicating whether a handshake reference is present.
 188    /// </summary>
 2189    public bool HasHandshakeReference => HandshakeId is not null;
 190
 191    /// <summary>
 192    /// Gets a value indicating whether additional metadata is present.
 193    /// </summary>
 2194    public bool HasMetadata => Metadata.Count > 0;
 195
 196    /// <summary>
 197    /// Creates a provider-neutral capability grant.
 198    /// </summary>
 199    public static CapabilityGrant Create(
 200        string tokenId,
 201        string issuer,
 202        string audience,
 203        IEnumerable<string> scopes,
 204        DateTimeOffset issuedUtc,
 205        DateTimeOffset expiresUtc,
 206        DateTimeOffset? notBeforeUtc = null,
 207        string? subjectId = null,
 208        string? operationName = null,
 209        string? policyVersion = null,
 210        string? policyHash = null,
 211        string? acknowledgmentId = null,
 212        string? handshakeId = null,
 213        string? gatewayBinding = null,
 214        string? resourceBinding = null,
 215        IReadOnlyDictionary<string, string>? metadata = null,
 216        int? maxUseCount = null,
 217        string? schemaVersion = null)
 218    {
 161219        return maxUseCount is < 1
 161220            ? throw new ArgumentOutOfRangeException(nameof(maxUseCount), maxUseCount, "A bound use limit must be greater
 161221            : new CapabilityGrant(
 161222            tokenId,
 161223            issuer,
 161224            audience,
 161225            NormalizeScopes(scopes),
 161226            issuedUtc,
 161227            notBeforeUtc,
 161228            expiresUtc,
 161229            subjectId,
 161230            operationName,
 161231            policyVersion,
 161232            policyHash,
 161233            acknowledgmentId,
 161234            handshakeId,
 161235            gatewayBinding,
 161236            resourceBinding,
 161237            NormalizeMetadata(metadata),
 161238            maxUseCount,
 161239            schemaVersion);
 240    }
 241
 242    private static ReadOnlyCollection<string> NormalizeScopes(IEnumerable<string> scopes)
 243    {
 160244        ArgumentNullException.ThrowIfNull(scopes);
 245
 159246        string[] normalizedScopes = [.. scopes
 159247            .Where(scope => !string.IsNullOrWhiteSpace(scope))
 159248            .Select(scope => scope.Trim())
 159249            .Distinct(StringComparer.Ordinal)
 159250            .OrderBy(scope => scope, StringComparer.Ordinal)];
 251
 159252        return normalizedScopes.Length == 0
 159253            ? EmptyScopes
 159254            : Array.AsReadOnly(normalizedScopes);
 255    }
 256
 257    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 258        IReadOnlyDictionary<string, string>? metadata)
 259    {
 159260        if (metadata is null || metadata.Count == 0)
 261        {
 154262            return EmptyMetadata;
 263        }
 264
 5265        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 266
 24267        foreach (KeyValuePair<string, string> item in metadata)
 268        {
 7269            if (string.IsNullOrWhiteSpace(item.Key))
 270            {
 271                continue;
 272            }
 273
 5274            normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 275        }
 276
 5277        return normalizedMetadata.Count == 0
 5278            ? EmptyMetadata
 5279            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 280    }
 281
 282    private static string? NormalizeOptional(string? value)
 283    {
 1248284        return string.IsNullOrWhiteSpace(value)
 1248285            ? null
 1248286            : value.Trim();
 287    }
 288}