| | | 1 | | using System.Collections.ObjectModel; |
| | | 2 | | using Microsoft.AspNetCore.Http; |
| | | 3 | | |
| | | 4 | | namespace AsiBackbone.AspNetCore.Endpoints; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Represents normalized AsiBackbone governance metadata resolved from an ASP.NET Core endpoint. |
| | | 8 | | /// </summary> |
| | | 9 | | public sealed class AsiBackboneEndpointGovernanceDescriptor |
| | | 10 | | { |
| | 3 | 11 | | private static readonly ReadOnlyCollection<Type> EmptyPolicyTypes = Array.AsReadOnly(Array.Empty<Type>()); |
| | 3 | 12 | | private static readonly ReadOnlyCollection<string> EmptyScopes = Array.AsReadOnly(Array.Empty<string>()); |
| | | 13 | | |
| | 35 | 14 | | private AsiBackboneEndpointGovernanceDescriptor( |
| | 35 | 15 | | string operationName, |
| | 35 | 16 | | IReadOnlyList<Type> policyTypes, |
| | 35 | 17 | | bool? shortCircuitOnFirstDenial, |
| | 35 | 18 | | bool requiresLiabilityHandshake, |
| | 35 | 19 | | IReadOnlyList<string> capabilityScopes, |
| | 35 | 20 | | bool emitGovernanceAudit) |
| | | 21 | | { |
| | 35 | 22 | | ArgumentException.ThrowIfNullOrWhiteSpace(operationName); |
| | | 23 | | |
| | 35 | 24 | | OperationName = operationName.Trim(); |
| | 35 | 25 | | PolicyTypes = policyTypes; |
| | 35 | 26 | | ShortCircuitOnFirstDenial = shortCircuitOnFirstDenial; |
| | 35 | 27 | | RequiresLiabilityHandshake = requiresLiabilityHandshake; |
| | 35 | 28 | | CapabilityScopes = capabilityScopes; |
| | 35 | 29 | | EmitGovernanceAudit = emitGovernanceAudit; |
| | 35 | 30 | | } |
| | | 31 | | |
| | | 32 | | /// <summary> |
| | | 33 | | /// Gets the operation name used for audit residue and acknowledgment challenge construction. |
| | | 34 | | /// </summary> |
| | 24 | 35 | | public string OperationName { get; } |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// Gets the policy marker or resolver types attached to the endpoint. |
| | | 39 | | /// </summary> |
| | 78 | 40 | | public IReadOnlyList<Type> PolicyTypes { get; } |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Gets an endpoint-scoped first-denial short-circuit preference, when endpoint metadata supplied one. |
| | | 44 | | /// </summary> |
| | 49 | 45 | | public bool? ShortCircuitOnFirstDenial { get; } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// Gets a value indicating whether liability-handshake support is requested. |
| | | 49 | | /// </summary> |
| | 37 | 50 | | public bool RequiresLiabilityHandshake { get; } |
| | | 51 | | |
| | | 52 | | /// <summary> |
| | | 53 | | /// Gets the required capability-grant scopes attached to the endpoint. |
| | | 54 | | /// </summary> |
| | 57 | 55 | | public IReadOnlyList<string> CapabilityScopes { get; } |
| | | 56 | | |
| | | 57 | | /// <summary> |
| | | 58 | | /// Gets a value indicating whether governance audit emission is requested. |
| | | 59 | | /// </summary> |
| | 30 | 60 | | public bool EmitGovernanceAudit { get; } |
| | | 61 | | |
| | | 62 | | /// <summary> |
| | | 63 | | /// Gets a value indicating whether the endpoint contains any AsiBackbone governance metadata. |
| | | 64 | | /// </summary> |
| | 35 | 65 | | public bool HasGovernanceMetadata => PolicyTypes.Count > 0 |
| | 35 | 66 | | || ShortCircuitOnFirstDenial.HasValue |
| | 35 | 67 | | || RequiresLiabilityHandshake |
| | 35 | 68 | | || CapabilityScopes.Count > 0 |
| | 35 | 69 | | || EmitGovernanceAudit; |
| | | 70 | | |
| | | 71 | | /// <summary> |
| | | 72 | | /// Creates a descriptor from the selected ASP.NET Core endpoint. |
| | | 73 | | /// </summary> |
| | | 74 | | /// <param name="endpoint">The selected endpoint.</param> |
| | | 75 | | /// <returns>A normalized descriptor.</returns> |
| | | 76 | | public static AsiBackboneEndpointGovernanceDescriptor FromEndpoint(Endpoint? endpoint) |
| | | 77 | | { |
| | 35 | 78 | | if (endpoint is null) |
| | | 79 | | { |
| | 0 | 80 | | return None("unresolved-endpoint"); |
| | | 81 | | } |
| | | 82 | | |
| | 35 | 83 | | Type[] policyTypes = [.. endpoint.Metadata |
| | 35 | 84 | | .GetOrderedMetadata<IAsiBackboneEndpointGovernancePolicyMetadata>() |
| | 15 | 85 | | .Select(metadata => metadata.PolicyType) |
| | 15 | 86 | | .Where(static policyType => policyType is not null) |
| | 35 | 87 | | .Distinct()]; |
| | | 88 | | |
| | 35 | 89 | | string[] capabilityScopes = [.. endpoint.Metadata |
| | 35 | 90 | | .GetOrderedMetadata<IAsiBackboneEndpointCapabilityGrantMetadata>() |
| | 18 | 91 | | .Select(metadata => metadata.Scope) |
| | 18 | 92 | | .Where(static scope => !string.IsNullOrWhiteSpace(scope)) |
| | 18 | 93 | | .Select(static scope => scope.Trim()) |
| | 35 | 94 | | .Distinct(StringComparer.Ordinal)]; |
| | | 95 | | |
| | 35 | 96 | | bool? shortCircuitOnFirstDenial = endpoint.Metadata |
| | 35 | 97 | | .GetOrderedMetadata<IAsiBackboneEndpointPolicyEvaluationOptionsMetadata>() |
| | 4 | 98 | | .Select(static metadata => metadata.ShortCircuitOnFirstDenial) |
| | 35 | 99 | | .LastOrDefault(); |
| | | 100 | | |
| | 35 | 101 | | bool requiresLiabilityHandshake = endpoint.Metadata |
| | 35 | 102 | | .GetOrderedMetadata<IAsiBackboneEndpointLiabilityHandshakeMetadata>() |
| | 37 | 103 | | .Any(static metadata => metadata.RequiresLiabilityHandshake); |
| | | 104 | | |
| | 35 | 105 | | bool emitGovernanceAudit = endpoint.Metadata |
| | 35 | 106 | | .GetOrderedMetadata<IAsiBackboneEndpointAuditEmissionMetadata>() |
| | 38 | 107 | | .Any(static metadata => metadata.EmitGovernanceAudit); |
| | | 108 | | |
| | 35 | 109 | | return new AsiBackboneEndpointGovernanceDescriptor( |
| | 35 | 110 | | ResolveOperationName(endpoint), |
| | 35 | 111 | | policyTypes.Length == 0 ? EmptyPolicyTypes : Array.AsReadOnly(policyTypes), |
| | 35 | 112 | | shortCircuitOnFirstDenial, |
| | 35 | 113 | | requiresLiabilityHandshake, |
| | 35 | 114 | | capabilityScopes.Length == 0 ? EmptyScopes : Array.AsReadOnly(capabilityScopes), |
| | 35 | 115 | | emitGovernanceAudit); |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | /// <summary> |
| | | 119 | | /// Creates a descriptor that does not request governance handling. |
| | | 120 | | /// </summary> |
| | | 121 | | /// <param name="operationName">The operation name to associate with the descriptor.</param> |
| | | 122 | | /// <returns>A descriptor with no governance metadata.</returns> |
| | | 123 | | public static AsiBackboneEndpointGovernanceDescriptor None(string operationName) |
| | | 124 | | { |
| | 0 | 125 | | return new AsiBackboneEndpointGovernanceDescriptor( |
| | 0 | 126 | | operationName, |
| | 0 | 127 | | EmptyPolicyTypes, |
| | 0 | 128 | | shortCircuitOnFirstDenial: null, |
| | 0 | 129 | | requiresLiabilityHandshake: false, |
| | 0 | 130 | | EmptyScopes, |
| | 0 | 131 | | emitGovernanceAudit: false); |
| | | 132 | | } |
| | | 133 | | |
| | | 134 | | /// <summary> |
| | | 135 | | /// Converts descriptor values into safe metadata for framework-neutral governance evaluation and audit residue. |
| | | 136 | | /// </summary> |
| | | 137 | | /// <returns>A normalized metadata dictionary.</returns> |
| | | 138 | | public IReadOnlyDictionary<string, string> ToMetadata() |
| | | 139 | | { |
| | 17 | 140 | | Dictionary<string, string> metadata = new(StringComparer.Ordinal) |
| | 17 | 141 | | { |
| | 17 | 142 | | ["endpoint.operation_name"] = OperationName, |
| | 17 | 143 | | ["endpoint.requires_liability_handshake"] = RequiresLiabilityHandshake ? "true" : "false", |
| | 17 | 144 | | ["endpoint.emit_governance_audit"] = EmitGovernanceAudit ? "true" : "false" |
| | 17 | 145 | | }; |
| | | 146 | | |
| | 17 | 147 | | if (PolicyTypes.Count > 0) |
| | | 148 | | { |
| | 22 | 149 | | metadata["endpoint.policy_types"] = string.Join(",", PolicyTypes.Select(static policyType => policyType.Full |
| | | 150 | | } |
| | | 151 | | |
| | 17 | 152 | | if (ShortCircuitOnFirstDenial.HasValue) |
| | | 153 | | { |
| | 4 | 154 | | metadata["endpoint.short_circuit_on_first_denial"] = ShortCircuitOnFirstDenial.Value ? "true" : "false"; |
| | | 155 | | } |
| | | 156 | | |
| | 17 | 157 | | if (CapabilityScopes.Count > 0) |
| | | 158 | | { |
| | 10 | 159 | | metadata["endpoint.capability_scopes"] = string.Join(",", CapabilityScopes); |
| | | 160 | | } |
| | | 161 | | |
| | 17 | 162 | | return metadata; |
| | | 163 | | } |
| | | 164 | | |
| | | 165 | | private static string ResolveOperationName(Endpoint endpoint) |
| | | 166 | | { |
| | 35 | 167 | | return string.IsNullOrWhiteSpace(endpoint.DisplayName) |
| | 35 | 168 | | ? "aspnetcore.endpoint" |
| | 35 | 169 | | : endpoint.DisplayName.Trim(); |
| | | 170 | | } |
| | | 171 | | } |