< Summary

Information
Class: AsiBackbone.AspNetCore.Acknowledgments.DefaultAcknowledgmentChallengeService
Assembly: AsiBackbone.AspNetCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Acknowledgments/DefaultAcknowledgmentChallengeService.cs
Line coverage
100%
Covered lines: 51
Uncovered lines: 0
Coverable lines: 51
Total lines: 136
Line coverage: 100%
Branch coverage
96%
Covered branches: 25
Total branches: 26
Branch coverage: 96.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%44100%
CreateChallenge(...)100%44100%
HandleResponse(...)100%1414100%
IsChallengedActor(...)75%44100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Acknowledgments/DefaultAcknowledgmentChallengeService.cs

#LineLine coverage
 1using AsiBackbone.Core.Acknowledgments;
 2using AsiBackbone.Core.Actors;
 3using AsiBackbone.Core.Decisions;
 4using Microsoft.Extensions.Options;
 5
 6namespace AsiBackbone.AspNetCore.Acknowledgments;
 7
 8/// <summary>
 9/// Provides the default ASP.NET Core-friendly acknowledgment challenge service.
 10/// </summary>
 11public sealed class DefaultAcknowledgmentChallengeService : IAcknowledgmentChallengeService
 12{
 13    private const string ChallengeMismatchCode = "acknowledgment.challenge.mismatch";
 14    private const string ChallengeActorMismatchCode = "acknowledgment.challenge.actor_mismatch";
 15    private const string ChallengeCodeMismatchCode = "acknowledgment.challenge.code_mismatch";
 16
 17    private readonly AcknowledgmentChallengeOptions options;
 18
 19    /// <summary>
 20    /// Initializes a new instance of the <see cref="DefaultAcknowledgmentChallengeService" /> class.
 21    /// </summary>
 22    /// <param name="options">The acknowledgment challenge options.</param>
 6823    public DefaultAcknowledgmentChallengeService(IOptions<AcknowledgmentChallengeOptions> options)
 24    {
 6825        this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
 6726        this.options.Validate();
 6627    }
 28
 29    /// <inheritdoc />
 30    public AcknowledgmentChallenge CreateChallenge(
 31        IGovernanceActorContext actor,
 32        string operationName,
 33        GovernanceDecision decision,
 34        IReadOnlyDictionary<string, string>? metadata = null)
 35    {
 3836        ArgumentNullException.ThrowIfNull(actor);
 3637        ArgumentNullException.ThrowIfNull(decision);
 38
 3539        if (!decision.RequiresAcknowledgment)
 40        {
 141            throw new InvalidOperationException("Only acknowledgment-required governance decisions can be converted into
 42        }
 43
 3444        if (!AcknowledgmentActorBinding.IsSufficient(actor))
 45        {
 646            throw new InvalidOperationException(
 647                $"{AcknowledgmentActorBinding.FailureCode}: {AcknowledgmentActorBinding.FailureMessage}");
 48        }
 49
 2850        var request = AcknowledgmentRequest.FromDecision(
 2851            actor,
 2852            operationName,
 2853            decision,
 2854            options.RequiredAcknowledgmentCode,
 2855            options.RequiredAcknowledgmentText,
 2856            options.RiskLevel,
 2857            options.RiskCategory,
 2858            metadata: metadata);
 59
 2860        return AcknowledgmentChallenge.FromAcknowledgmentRequest(request, options);
 61    }
 62
 63    /// <inheritdoc />
 64    public AcknowledgmentChallengeResult HandleResponse(
 65        AcknowledgmentChallenge challenge,
 66        IGovernanceActorContext actor,
 67        AcknowledgmentChallengeRequest response,
 68        DateTimeOffset? occurredUtc = null)
 69    {
 2570        ArgumentNullException.ThrowIfNull(challenge);
 2471        ArgumentNullException.ThrowIfNull(actor);
 2372        ArgumentNullException.ThrowIfNull(response);
 73
 2274        if (!string.Equals(challenge.HandshakeId, response.HandshakeId?.Trim(), StringComparison.Ordinal))
 75        {
 476            return AcknowledgmentChallengeResult.Failure(
 477                ChallengeMismatchCode,
 478                "The acknowledgment response did not match the active challenge.");
 79        }
 80
 1881        if (!AcknowledgmentActorBinding.IsSufficient(challenge.AcknowledgmentRequest)
 1882            || !AcknowledgmentActorBinding.IsSufficient(actor))
 83        {
 384            return AcknowledgmentChallengeResult.Failure(
 385                AcknowledgmentActorBinding.FailureCode,
 386                AcknowledgmentActorBinding.FailureMessage);
 87        }
 88
 89        // The acknowledgment is the accountability record for the operation, so it must be produced by the actor the
 90        // challenge was issued to. Previously any actor could satisfy any challenge it could name, and the persisted
 91        // acknowledgment attributed the liability to whoever answered rather than to whoever was challenged.
 1592        if (!IsChallengedActor(challenge, actor))
 93        {
 394            return AcknowledgmentChallengeResult.Failure(
 395                ChallengeActorMismatchCode,
 396                "The acknowledgment response was not submitted by the actor the challenge was issued to.");
 97        }
 98
 1299        if (!string.Equals(challenge.RequiredAcknowledgmentCode, response.AcknowledgmentCode?.Trim(), StringComparison.O
 100        {
 6101            return AcknowledgmentChallengeResult.Failure(
 6102                ChallengeCodeMismatchCode,
 6103                "The acknowledgment response did not contain the required acknowledgment code.");
 104        }
 105
 6106        var acknowledgment = AcknowledgmentResponse.Create(
 6107            challenge.AcknowledgmentRequest,
 6108            actor,
 6109            response.Acknowledged,
 6110            occurredUtc: occurredUtc,
 6111            metadata: response.Metadata);
 112
 6113        return AcknowledgmentChallengeResult.Success(acknowledgment);
 114    }
 115
 116    /// <summary>
 117    /// Determines whether the responding actor is the actor the challenge was issued to.
 118    /// </summary>
 119    /// <remarks>
 120    /// Both the identifier and the actor type participate, because the same identifier under a different actor type is 
 121    /// different principal. A single reason code covers both comparisons so a caller cannot use the failure to probe
 122    /// which component differed.
 123    /// </remarks>
 124    /// <param name="challenge">The active acknowledgment challenge.</param>
 125    /// <param name="actor">The actor that submitted the acknowledgment response.</param>
 126    /// <returns><see langword="true" /> when the responding actor matches the challenged actor.</returns>
 127    private static bool IsChallengedActor(
 128        AcknowledgmentChallenge challenge,
 129        IGovernanceActorContext actor)
 130    {
 15131        AcknowledgmentRequest request = challenge.AcknowledgmentRequest;
 132
 15133        return string.Equals(request.ActorId, actor.ActorId?.Trim(), StringComparison.Ordinal)
 15134            && request.ActorType == actor.ActorType;
 135    }
 136}