< 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{
 215    private static readonly ReadOnlyCollection<string> EmptyScopes =
 216        Array.AsReadOnly(Array.Empty<string>());
 17
 218    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 219        new ReadOnlyDictionary<string, string>(
 220            new Dictionary<string, string>(StringComparer.Ordinal));
 21
 12422    private CapabilityTokenGrant(
 12423        string tokenId,
 12424        string issuer,
 12425        string audience,
 12426        IReadOnlyList<string> scopes,
 12427        DateTimeOffset issuedUtc,
 12428        DateTimeOffset? notBeforeUtc,
 12429        DateTimeOffset expiresUtc,
 12430        string? subjectId,
 12431        string? operationName,
 12432        string? policyVersion,
 12433        string? policyHash,
 12434        string? acknowledgmentId,
 12435        string? handshakeId,
 12436        string? gatewayBinding,
 12437        string? resourceBinding,
 12438        IReadOnlyDictionary<string, string> metadata,
 12439        string? schemaVersion)
 40    {
 12441        ArgumentException.ThrowIfNullOrWhiteSpace(tokenId);
 12442        ArgumentException.ThrowIfNullOrWhiteSpace(issuer);
 12443        ArgumentException.ThrowIfNullOrWhiteSpace(audience);
 12444        ArgumentNullException.ThrowIfNull(scopes);
 45
 12446        if (scopes.Count == 0)
 47        {
 248            throw new ArgumentException("At least one capability scope is required.", nameof(scopes));
 49        }
 50
 12251        DateTimeOffset normalizedIssuedUtc = issuedUtc.ToUniversalTime();
 12252        DateTimeOffset? normalizedNotBeforeUtc = notBeforeUtc?.ToUniversalTime();
 12253        DateTimeOffset normalizedExpiresUtc = expiresUtc.ToUniversalTime();
 54
 12255        if (normalizedNotBeforeUtc.HasValue && normalizedNotBeforeUtc.Value > normalizedExpiresUtc)
 56        {
 257            throw new ArgumentOutOfRangeException(nameof(notBeforeUtc), notBeforeUtc, "Not-before time must be earlier t
 58        }
 59
 12060        if (normalizedIssuedUtc > normalizedExpiresUtc)
 61        {
 262            throw new ArgumentOutOfRangeException(nameof(expiresUtc), expiresUtc, "Expiration time must be later than or
 63        }
 64
 11865        TokenId = tokenId.Trim();
 11866        Issuer = issuer.Trim();
 11867        Audience = audience.Trim();
 11868        Scopes = scopes;
 11869        IssuedUtc = normalizedIssuedUtc;
 11870        NotBeforeUtc = normalizedNotBeforeUtc;
 11871        ExpiresUtc = normalizedExpiresUtc;
 11872        SubjectId = NormalizeOptional(subjectId);
 11873        OperationName = NormalizeOptional(operationName);
 11874        PolicyVersion = NormalizeOptional(policyVersion);
 11875        PolicyHash = NormalizeOptional(policyHash);
 11876        AcknowledgmentId = NormalizeOptional(acknowledgmentId);
 11877        HandshakeId = NormalizeOptional(handshakeId);
 11878        GatewayBinding = NormalizeOptional(gatewayBinding);
 11879        ResourceBinding = NormalizeOptional(resourceBinding);
 11880        Metadata = metadata;
 11881        SchemaVersion = AsiBackboneSchemaVersions.Normalize(schemaVersion);
 11882    }
 83
 84    /// <summary>
 85    /// Gets the stable grant identifier used for validation and replay checks.
 86    /// </summary>
 39487    public string TokenId { get; }
 88
 89    /// <summary>
 90    /// Gets the issuer that created the grant.
 91    /// </summary>
 27492    public string Issuer { get; }
 93
 94    /// <summary>
 95    /// Gets the intended audience for the grant.
 96    /// </summary>
 27297    public string Audience { get; }
 98
 99    /// <summary>
 100    /// Gets the least-privilege scopes carried by the grant.
 101    /// </summary>
 150102    public IReadOnlyList<string> Scopes { get; }
 103
 104    /// <summary>
 105    /// Gets the UTC timestamp when the grant was issued.
 106    /// </summary>
 2107    public DateTimeOffset IssuedUtc { get; }
 108
 109    /// <summary>
 110    /// Gets the UTC timestamp before which the grant is not valid.
 111    /// </summary>
 92112    public DateTimeOffset? NotBeforeUtc { get; }
 113
 114    /// <summary>
 115    /// Gets the UTC timestamp when the grant expires.
 116    /// </summary>
 174117    public DateTimeOffset ExpiresUtc { get; }
 118
 119    /// <summary>
 120    /// Gets the host-defined subject identifier, when supplied.
 121    /// </summary>
 4122    public string? SubjectId { get; }
 123
 124    /// <summary>
 125    /// Gets the operation name or action family the grant is intended to authorize.
 126    /// </summary>
 4127    public string? OperationName { get; }
 128
 129    /// <summary>
 130    /// Gets the policy version bound to the grant, when supplied.
 131    /// </summary>
 150132    public string? PolicyVersion { get; }
 133
 134    /// <summary>
 135    /// Gets the policy hash bound to the grant, when supplied.
 136    /// </summary>
 148137    public string? PolicyHash { get; }
 138
 139    /// <summary>
 140    /// Gets the acknowledgment identifier bound to the grant, when supplied.
 141    /// </summary>
 140142    public string? AcknowledgmentId { get; }
 143
 144    /// <summary>
 145    /// Gets the handshake identifier bound to the grant, when supplied.
 146    /// </summary>
 136147    public string? HandshakeId { get; }
 148
 149    /// <summary>
 150    /// Gets the optional gateway binding used to limit execution context.
 151    /// </summary>
 26152    public string? GatewayBinding { get; }
 153
 154    /// <summary>
 155    /// Gets the optional resource binding used to limit the target resource.
 156    /// </summary>
 130157    public string? ResourceBinding { get; }
 158
 159    /// <summary>
 160    /// Gets the canonical schema version for this grant.
 161    /// </summary>
 98162    public string SchemaVersion { get; }
 163
 164    /// <summary>
 165    /// Gets provider-neutral metadata carried with the grant.
 166    /// </summary>
 12167    public IReadOnlyDictionary<string, string> Metadata { get; }
 168
 169    /// <summary>
 170    /// Gets a value indicating whether an acknowledgment reference is present.
 171    /// </summary>
 6172    public bool HasAcknowledgmentReference => AcknowledgmentId is not null;
 173
 174    /// <summary>
 175    /// Gets a value indicating whether a handshake reference is present.
 176    /// </summary>
 4177    public bool HasHandshakeReference => HandshakeId is not null;
 178
 179    /// <summary>
 180    /// Gets a value indicating whether additional metadata is present.
 181    /// </summary>
 4182    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    {
 126206        return new CapabilityTokenGrant(
 126207            tokenId,
 126208            issuer,
 126209            audience,
 126210            NormalizeScopes(scopes),
 126211            issuedUtc,
 126212            notBeforeUtc,
 126213            expiresUtc,
 126214            subjectId,
 126215            operationName,
 126216            policyVersion,
 126217            policyHash,
 126218            acknowledgmentId,
 126219            handshakeId,
 126220            gatewayBinding,
 126221            resourceBinding,
 126222            NormalizeMetadata(metadata),
 126223            schemaVersion);
 224    }
 225
 226    private static ReadOnlyCollection<string> NormalizeScopes(IEnumerable<string> scopes)
 227    {
 126228        ArgumentNullException.ThrowIfNull(scopes);
 229
 124230        string[] normalizedScopes = [.. scopes
 138231            .Where(scope => !string.IsNullOrWhiteSpace(scope))
 132232            .Select(scope => scope.Trim())
 124233            .Distinct(StringComparer.Ordinal)
 138234            .OrderBy(scope => scope, StringComparer.Ordinal)];
 235
 124236        return normalizedScopes.Length == 0
 124237            ? EmptyScopes
 124238            : Array.AsReadOnly(normalizedScopes);
 239    }
 240
 241    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 242        IReadOnlyDictionary<string, string>? metadata)
 243    {
 124244        if (metadata is null || metadata.Count == 0)
 245        {
 120246            return EmptyMetadata;
 247        }
 248
 4249        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 250
 24251        foreach (KeyValuePair<string, string> item in metadata)
 252        {
 8253            if (string.IsNullOrWhiteSpace(item.Key))
 254            {
 255                continue;
 256            }
 257
 4258            normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 259        }
 260
 4261        return normalizedMetadata.Count == 0
 4262            ? EmptyMetadata
 4263            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 264    }
 265
 266    private static string? NormalizeOptional(string? value)
 267    {
 944268        return string.IsNullOrWhiteSpace(value)
 944269            ? null
 944270            : value.Trim();
 271    }
 272}