Require registered Bifrost identity for bridge mouths
This commit is contained in:
parent
23c02be2a6
commit
80a5392935
2 changed files with 104 additions and 0 deletions
|
|
@ -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<BridgePolicyDecision?> 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<BridgePolicyDecision?> EvaluateBifrostOwnedProvenanceAsync(
|
||||
BridgeActionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
|
|
|
|||
|
|
@ -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<TestWebApplicationFac
|
|||
public async Task Persona_discord_action_requires_heimdall_reference_after_bifrost_identity()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
await EnsureBifrostIdentityRegisteredAsync("epiphany.Persona");
|
||||
|
||||
var requestPayload = JsonSerializer.Serialize(new
|
||||
{
|
||||
|
|
@ -836,10 +838,52 @@ public sealed class PlatformEndpointsTests : IClassFixture<TestWebApplicationFac
|
|||
Assert.Contains("Heimdall", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Persona_discord_action_with_unregistered_bifrost_identity_is_denied()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var identity = $"unregistered.{Guid.NewGuid():N}";
|
||||
|
||||
var requestPayload = JsonSerializer.Serialize(new
|
||||
{
|
||||
actorKind = "Persona",
|
||||
actorName = "Epiphany Persona",
|
||||
targetSurface = "Discord",
|
||||
actionKind = "DiscordPost",
|
||||
targetLocator = "channel/1501196543150264332",
|
||||
sourceKind = "epiphany_persona_speech",
|
||||
sourceId = "persona-speech-audit-unregistered-identity",
|
||||
authorityReference = "epiphany.persona_speech_audit",
|
||||
bifrostIdentity = identity,
|
||||
heimdallCapabilityReference = "heimdall:discord:capability:epiphany-persona",
|
||||
title = "Persona Discord post",
|
||||
summary = "This should not pass with an unregistered Bifrost identity."
|
||||
});
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
|
||||
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
|
||||
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
|
||||
Assert.NotNull(action);
|
||||
Assert.Equal("Denied", action.Status);
|
||||
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||
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<TestWebApplicationFac
|
|||
$"Expected governance receipt seed to succeed but got {(int)response.StatusCode}: {body}");
|
||||
}
|
||||
|
||||
private async Task EnsureBifrostIdentityRegisteredAsync(string identity)
|
||||
{
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||
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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue