diff --git a/src/Bifrost.Web/Features/Bridge/BridgeActionService.cs b/src/Bifrost.Web/Features/Bridge/BridgeActionService.cs index b1da506..7f3e615 100644 --- a/src/Bifrost.Web/Features/Bridge/BridgeActionService.cs +++ b/src/Bifrost.Web/Features/Bridge/BridgeActionService.cs @@ -1,5 +1,6 @@ using Bifrost.Web.Data; using Bifrost.Web.Domain; +using Bifrost.Web.Features.Membership; using Bifrost.Web.Features.Shared; using Microsoft.EntityFrameworkCore; @@ -212,6 +213,12 @@ public sealed class BridgeActionService( return BridgePolicyDecision.Reject("Persona and agent Discord/Reddit bridge actions must carry a Bifrost identity."); } + var identityDecision = await EvaluateRegisteredBifrostIdentityAsync(request.BifrostIdentity, cancellationToken); + if (identityDecision is not null) + { + return identityDecision; + } + if (string.IsNullOrWhiteSpace(request.HeimdallCapabilityReference)) { return BridgePolicyDecision.Reject("Persona and agent Discord/Reddit bridge actions must carry a Heimdall-backed capability or account reference."); @@ -257,6 +264,31 @@ public sealed class BridgeActionService( : "Authorized for an active Bifrost member through Bifrost policy."); } + private async Task EvaluateRegisteredBifrostIdentityAsync( + string bifrostIdentity, + CancellationToken cancellationToken) + { + string normalizedIdentity; + try + { + normalizedIdentity = BifrostIdentityService.NormalizeIdentity(bifrostIdentity); + } + catch (BifrostIdentityException exception) + { + return BridgePolicyDecision.Reject(exception.Message); + } + + var exists = await dbContext.UserAccounts.AnyAsync( + x => x.NormalizedBifrostIdentity == normalizedIdentity, + cancellationToken); + if (!exists) + { + return BridgePolicyDecision.Reject("Persona and agent Discord/Reddit bridge actions must carry a registered Bifrost identity."); + } + + return null; + } + private async Task EvaluateBifrostOwnedProvenanceAsync( BridgeActionRequest request, CancellationToken cancellationToken) diff --git a/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs b/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs index 21f9efd..f74c3e0 100644 --- a/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs +++ b/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs @@ -6,6 +6,7 @@ using System.Text.Json; using Bifrost.Web.Features.Patronage; using Bifrost.Web.Data; using Bifrost.Web.Domain; +using Bifrost.Web.Features.Membership; using Bifrost.Web.Tests.Support; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; @@ -801,6 +802,7 @@ public sealed class PlatformEndpointsTests : IClassFixture(); + Assert.NotNull(action); + Assert.Equal("Denied", action.Status); + + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id); + + Assert.Equal(BridgeActionStatus.Denied, savedAction.Status); + Assert.Contains("registered Bifrost identity", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task Persona_reddit_action_with_bifrost_identity_and_heimdall_reference_is_authorized_and_stored() { using var client = _factory.CreateClient(); + await EnsureBifrostIdentityRegisteredAsync("epiphany.Persona"); var requestPayload = JsonSerializer.Serialize(new { @@ -1357,6 +1401,34 @@ public sealed class PlatformEndpointsTests : IClassFixture(); + var normalizedIdentity = BifrostIdentityService.NormalizeIdentity(identity); + if (await dbContext.UserAccounts.AnyAsync(x => x.NormalizedBifrostIdentity == normalizedIdentity)) + { + return; + } + + var now = DateTimeOffset.UtcNow; + dbContext.UserAccounts.Add(new UserAccount + { + BifrostIdentity = normalizedIdentity, + NormalizedBifrostIdentity = normalizedIdentity, + DisplayName = normalizedIdentity, + CreatedAtUtc = now, + LastSeenAtUtc = now, + Membership = new Bifrost.Web.Domain.Membership + { + Status = MembershipStatus.Authenticated, + CreatedAtUtc = now, + Notes = "Registered test Bifrost identity" + } + }); + await dbContext.SaveChangesAsync(); + } + private sealed class BridgeActionHttpResult { public Guid Id { get; init; }