< Summary

Information
Class: AsiBackbone.Core.CapabilityTokens.CapabilityTokenGrant
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityTokens/CapabilityTokenGrant.cs
Line coverage
100%
Covered lines: 113
Uncovered lines: 0
Coverable lines: 113
Total lines: 272
Line coverage: 100%
Branch coverage
100%
Covered branches: 28
Total branches: 28
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_TokenId()100%11100%
get_Issuer()100%11100%
get_Audience()100%11100%
get_Scopes()100%11100%
get_IssuedUtc()100%11100%
get_NotBeforeUtc()100%11100%
get_ExpiresUtc()100%11100%
get_SubjectId()100%11100%
get_OperationName()100%11100%
get_PolicyVersion()100%11100%
get_PolicyHash()100%11100%
get_AcknowledgmentId()100%11100%
get_HandshakeId()100%11100%
get_GatewayBinding()100%11100%
get_ResourceBinding()100%11100%
get_SchemaVersion()100%11100%
get_Metadata()100%11100%
get_HasAcknowledgmentReference()100%11100%
get_HasHandshakeReference()100%11100%
get_HasMetadata()100%11100%
Create(...)100%11100%
NormalizeScopes(...)100%22100%
NormalizeMetadata(...)100%1414100%
NormalizeOptional(...)100%22100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityTokens/CapabilityTokenGrant.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using AsiBackbone.Core.Serialization;
 3
 4namespace AsiBackbone.Core.CapabilityTokens;
 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 CapabilityTokenGrant
 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
 4222    private CapabilityTokenGrant(
 4223        string tokenId,
 4224        string issuer,
 4225        string audience,
 4226        IReadOnlyList<string> scopes,
 4227        DateTimeOffset issuedUtc,
 4228        DateTimeOffset? notBeforeUtc,
 4229        DateTimeOffset expiresUtc,
 4230        string? subjectId,
 4231        string? operationName,
 4232        string? policyVersion,
 4233        string? policyHash,
 4234        string? acknowledgmentId,
 4235        string? handshakeId,
 4236        string? gatewayBinding,
 4237        string? resourceBinding,
 4238        IReadOnlyDictionary<string, string> metadata,
 4239        string? schemaVersion)
 40    {
 4241        ArgumentException.ThrowIfNullOrWhiteSpace(tokenId);
 4242        ArgumentException.ThrowIfNullOrWhiteSpace(issuer);
 4243        ArgumentException.ThrowIfNullOrWhiteSpace(audience);
 4244        ArgumentNullException.ThrowIfNull(scopes);
 45
 4246        if (scopes.Count == 0)
 47        {
 148            throw new ArgumentException("At least one capability scope is required.", nameof(scopes));
 49        }
 50
 4151        DateTimeOffset normalizedIssuedUtc = issuedUtc.ToUniversalTime();
 4152        DateTimeOffset? normalizedNotBeforeUtc = notBeforeUtc?.ToUniversalTime();
 4153        DateTimeOffset normalizedExpiresUtc = expiresUtc.ToUniversalTime();
 54
 4155        if (normalizedNotBeforeUtc.HasValue && normalizedNotBeforeUtc.Value > normalizedExpiresUtc)
 56        {
 157            throw new ArgumentOutOfRangeException(nameof(notBeforeUtc), notBeforeUtc, "Not-before time must be earlier t
 58        }
 59
 4060        if (normalizedIssuedUtc > normalizedExpiresUtc)
 61        {
 162            throw new ArgumentOutOfRangeException(nameof(expiresUtc), expiresUtc, "Expiration time must be later than or
 63        }
 64
 3965        TokenId = tokenId.Trim();
 3966        Issuer = issuer.Trim();
 3967        Audience = audience.Trim();
 3968        Scopes = scopes;
 3969        IssuedUtc = normalizedIssuedUtc;
 3970        NotBeforeUtc = normalizedNotBeforeUtc;
 3971        ExpiresUtc = normalizedExpiresUtc;
 3972        SubjectId = NormalizeOptional(subjectId);
 3973        OperationName = NormalizeOptional(operationName);
 3974        PolicyVersion = NormalizeOptional(policyVersion);
 3975        PolicyHash = NormalizeOptional(policyHash);
 3976        AcknowledgmentId = NormalizeOptional(acknowledgmentId);
 3977        HandshakeId = NormalizeOptional(handshakeId);
 3978        GatewayBinding = NormalizeOptional(gatewayBinding);
 3979        ResourceBinding = NormalizeOptional(resourceBinding);
 3980        Metadata = metadata;
 3981        SchemaVersion = AsiBackboneSchemaVersions.Normalize(schemaVersion);
 3982    }
 83
 84    /// <summary>
 85    /// Gets the stable grant identifier used for validation and replay checks.
 86    /// </summary>
 12087    public string TokenId { get; }
 88
 89    /// <summary>
 90    /// Gets the issuer that created the grant.
 91    /// </summary>
 9392    public string Issuer { get; }
 93
 94    /// <summary>
 95    /// Gets the intended audience for the grant.
 96    /// </summary>
 9297    public string Audience { get; }
 98
 99    /// <summary>
 100    /// Gets the least-privilege scopes carried by the grant.
 101    /// </summary>
 53102    public IReadOnlyList<string> Scopes { get; }
 103
 104    /// <summary>
 105    /// Gets the UTC timestamp when the grant was issued.
 106    /// </summary>
 1107    public DateTimeOffset IssuedUtc { get; }
 108
 109    /// <summary>
 110    /// Gets the UTC timestamp before which the grant is not valid.
 111    /// </summary>
 23112    public DateTimeOffset? NotBeforeUtc { get; }
 113
 114    /// <summary>
 115    /// Gets the UTC timestamp when the grant expires.
 116    /// </summary>
 54117    public DateTimeOffset ExpiresUtc { get; }
 118
 119    /// <summary>
 120    /// Gets the host-defined subject identifier, when supplied.
 121    /// </summary>
 2122    public string? SubjectId { get; }
 123
 124    /// <summary>
 125    /// Gets the operation name or action family the grant is intended to authorize.
 126    /// </summary>
 2127    public string? OperationName { get; }
 128
 129    /// <summary>
 130    /// Gets the policy version bound to the grant, when supplied.
 131    /// </summary>
 55132    public string? PolicyVersion { get; }
 133
 134    /// <summary>
 135    /// Gets the policy hash bound to the grant, when supplied.
 136    /// </summary>
 54137    public string? PolicyHash { get; }
 138
 139    /// <summary>
 140    /// Gets the acknowledgment identifier bound to the grant, when supplied.
 141    /// </summary>
 53142    public string? AcknowledgmentId { get; }
 143
 144    /// <summary>
 145    /// Gets the handshake identifier bound to the grant, when supplied.
 146    /// </summary>
 51147    public string? HandshakeId { get; }
 148
 149    /// <summary>
 150    /// Gets the optional gateway binding used to limit execution context.
 151    /// </summary>
 13152    public string? GatewayBinding { get; }
 153
 154    /// <summary>
 155    /// Gets the optional resource binding used to limit the target resource.
 156    /// </summary>
 48157    public string? ResourceBinding { get; }
 158
 159    /// <summary>
 160    /// Gets the canonical schema version for this grant.
 161    /// </summary>
 33162    public string SchemaVersion { get; }
 163
 164    /// <summary>
 165    /// Gets provider-neutral metadata carried with the grant.
 166    /// </summary>
 6167    public IReadOnlyDictionary<string, string> Metadata { get; }
 168
 169    /// <summary>
 170    /// Gets a value indicating whether an acknowledgment reference is present.
 171    /// </summary>
 3172    public bool HasAcknowledgmentReference => AcknowledgmentId is not null;
 173
 174    /// <summary>
 175    /// Gets a value indicating whether a handshake reference is present.
 176    /// </summary>
 2177    public bool HasHandshakeReference => HandshakeId is not null;
 178
 179    /// <summary>
 180    /// Gets a value indicating whether additional metadata is present.
 181    /// </summary>
 2182    public bool HasMetadata => Metadata.Count > 0;
 183
 184    /// <summary>
 185    /// Creates a provider-neutral capability grant.
 186    /// </summary>
 187    public static CapabilityTokenGrant Create(
 188        string tokenId,
 189        string issuer,
 190        string audience,
 191        IEnumerable<string> scopes,
 192        DateTimeOffset issuedUtc,
 193        DateTimeOffset expiresUtc,
 194        DateTimeOffset? notBeforeUtc = null,
 195        string? subjectId = null,
 196        string? operationName = null,
 197        string? policyVersion = null,
 198        string? policyHash = null,
 199        string? acknowledgmentId = null,
 200        string? handshakeId = null,
 201        string? gatewayBinding = null,
 202        string? resourceBinding = null,
 203        IReadOnlyDictionary<string, string>? metadata = null,
 204        string? schemaVersion = null)
 205    {
 43206        return new CapabilityTokenGrant(
 43207            tokenId,
 43208            issuer,
 43209            audience,
 43210            NormalizeScopes(scopes),
 43211            issuedUtc,
 43212            notBeforeUtc,
 43213            expiresUtc,
 43214            subjectId,
 43215            operationName,
 43216            policyVersion,
 43217            policyHash,
 43218            acknowledgmentId,
 43219            handshakeId,
 43220            gatewayBinding,
 43221            resourceBinding,
 43222            NormalizeMetadata(metadata),
 43223            schemaVersion);
 224    }
 225
 226    private static ReadOnlyCollection<string> NormalizeScopes(IEnumerable<string> scopes)
 227    {
 43228        ArgumentNullException.ThrowIfNull(scopes);
 229
 42230        string[] normalizedScopes = [.. scopes
 49231            .Where(scope => !string.IsNullOrWhiteSpace(scope))
 46232            .Select(scope => scope.Trim())
 42233            .Distinct(StringComparer.Ordinal)
 49234            .OrderBy(scope => scope, StringComparer.Ordinal)];
 235
 42236        return normalizedScopes.Length == 0
 42237            ? EmptyScopes
 42238            : Array.AsReadOnly(normalizedScopes);
 239    }
 240
 241    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 242        IReadOnlyDictionary<string, string>? metadata)
 243    {
 42244        if (metadata is null || metadata.Count == 0)
 245        {
 40246            return EmptyMetadata;
 247        }
 248
 2249        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 250
 12251        foreach (KeyValuePair<string, string> item in metadata)
 252        {
 4253            if (string.IsNullOrWhiteSpace(item.Key))
 254            {
 255                continue;
 256            }
 257
 2258            normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 259        }
 260
 2261        return normalizedMetadata.Count == 0
 2262            ? EmptyMetadata
 2263            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 264    }
 265
 266    private static string? NormalizeOptional(string? value)
 267    {
 312268        return string.IsNullOrWhiteSpace(value)
 312269            ? null
 312270            : value.Trim();
 271    }
 272}