Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Ingestion/IngestionProposalAuthorizationTests.cs
T

219 lines
9.2 KiB
C#

using System.Net.Http.Json;
using System.Security.Claims;
using ItWorx.KnowledgeHub.Api.Authorization;
using ItWorx.KnowledgeHub.Application.Ingestion;
using ItWorx.KnowledgeHub.Contracts.Admin;
using ItWorx.KnowledgeHub.Contracts.Auth;
using ItWorx.KnowledgeHub.Contracts.Automation;
using ItWorx.KnowledgeHub.Domain.Automation;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace ItWorx.KnowledgeHub.IntegrationTests.Ingestion;
/// <summary>
/// Covers the bounded Task 3 surface without registering proposal routes that Task 4
/// owns. The policy probe invokes the application's registered authorization policies
/// against the same claims emitted by the service-token handler.
/// </summary>
[Collection(PostgresTestGroup.Name)]
public sealed class IngestionProposalAuthorizationTests : OwnerSetupTestBase
{
private readonly PostgresContainerFixture _postgres;
public IngestionProposalAuthorizationTests(PostgresContainerFixture postgres) : base(postgres)
{
_postgres = postgres;
}
[Fact]
public async Task Ingestion_write_scope_satisfies_submission_but_not_human_review()
{
using var scope = Factory.Services.CreateScope();
var authorization = scope.ServiceProvider.GetRequiredService<IAuthorizationService>();
var ingestingService = ServicePrincipal(Scopes.IngestionWrite);
Assert.True((await authorization.AuthorizeAsync(
ingestingService, resource: null, AuthorizationPolicies.IngestionWrite)).Succeeded);
Assert.False((await authorization.AuthorizeAsync(
ingestingService, resource: null, AuthorizationPolicies.IngestionReview)).Succeeded);
}
[Fact]
public async Task Pages_write_scope_cannot_satisfy_ingestion_submission()
{
using var scope = Factory.Services.CreateScope();
var authorization = scope.ServiceProvider.GetRequiredService<IAuthorizationService>();
var pagesService = ServicePrincipal(Scopes.PagesWrite);
Assert.False((await authorization.AuthorizeAsync(
pagesService, resource: null, AuthorizationPolicies.IngestionWrite)).Succeeded);
}
[Fact]
public async Task A_delegated_principal_with_ingestion_write_can_submit_but_cannot_review()
{
using var scope = Factory.Services.CreateScope();
var authorization = scope.ServiceProvider.GetRequiredService<IAuthorizationService>();
var delegatedIdentity = new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Name, "delegated-human")
], authenticationType: "delegated-cookie");
var serviceIdentity = new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new Claim(ServiceTokenAuthenticationHandler.ServiceAccountClaimType, Guid.NewGuid().ToString()),
new Claim(ServiceTokenAuthenticationHandler.ScopeClaimType, Scopes.IngestionWrite)
], ServiceTokenAuthenticationDefaults.Scheme);
var delegatedService = new ClaimsPrincipal([delegatedIdentity, serviceIdentity]);
Assert.True((await authorization.AuthorizeAsync(
delegatedService, resource: null, AuthorizationPolicies.IngestionWrite)).Succeeded);
Assert.False((await authorization.AuthorizeAsync(
delegatedService, resource: null, AuthorizationPolicies.IngestionReview)).Succeeded);
}
[Fact]
public async Task Default_options_disable_ingestion_and_anonymous_session_has_no_features_or_workspace()
{
using var scope = Factory.Services.CreateScope();
var options = scope.ServiceProvider.GetRequiredService<IOptions<IngestionProposalOptions>>().Value;
Assert.False(options.Enabled);
using var client = Factory.CreateClient();
var session = await client.GetFromJsonAsync<SessionResponse>("/api/v1/auth/session");
Assert.NotNull(session);
Assert.False(session.Authenticated);
Assert.Null(session.Features);
Assert.Null(session.WorkspaceId);
Assert.Null(session.Role);
}
[Fact]
public async Task Authenticated_default_configuration_session_exposes_disabled_ingestion_feature()
{
using var client = Factory.CreateClient();
var login = await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
login.EnsureSuccessStatusCode();
var session = await login.Content.ReadFromJsonAsync<SessionResponse>();
Assert.NotNull(session);
Assert.True(session.Authenticated);
Assert.NotNull(session.Features);
Assert.False(session.Features.IngestionProposals);
}
[Fact]
public async Task An_authenticated_human_editor_can_submit_and_review()
{
using var administratorClient = Factory.CreateClient();
var administratorLogin = await SetupHelper.LoginAsync(administratorClient, OwnerEmail, OwnerPassword);
administratorLogin.EnsureSuccessStatusCode();
const string editorEmail = "ingestion-editor@itworx.test";
const string editorPassword = "editor-strong-password";
var createEditor = await SetupHelper.PostWithCsrfAsync(
administratorClient,
"/api/v1/admin/users",
new CreateUserRequest("Ingestion Editor", editorEmail, editorPassword, "Editor"));
createEditor.EnsureSuccessStatusCode();
using var editorClient = Factory.CreateClient();
var editorLogin = await SetupHelper.LoginAsync(editorClient, editorEmail, editorPassword);
editorLogin.EnsureSuccessStatusCode();
var session = await editorLogin.Content.ReadFromJsonAsync<SessionResponse>();
Assert.NotNull(session?.UserId);
Assert.Equal("Editor", session.Role);
using var scope = Factory.Services.CreateScope();
var accessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
accessor.HttpContext = new DefaultHttpContext { User = HumanPrincipal(session.UserId.Value) };
try
{
var authorization = scope.ServiceProvider.GetRequiredService<IAuthorizationService>();
var human = HumanPrincipal(session.UserId.Value);
Assert.True((await authorization.AuthorizeAsync(
human, resource: null, AuthorizationPolicies.IngestionWrite)).Succeeded);
Assert.True((await authorization.AuthorizeAsync(
human, resource: null, AuthorizationPolicies.IngestionReview)).Succeeded);
}
finally
{
accessor.HttpContext = null;
}
}
[Fact]
public async Task Authenticated_session_exposes_only_the_server_configured_ingestion_feature()
{
var configuredFactory = new KnowledgeHubIntegrationFactory(_postgres,
new Dictionary<string, string?> { ["IngestionProposal:Enabled"] = "true" });
await ((IAsyncLifetime)configuredFactory).InitializeAsync();
try
{
using var client = configuredFactory.CreateClient();
var setup = await SetupHelper.CompleteFirstRunSetupAsync(
client, "Ingestion test", "Owner", "ingestion-owner@itworx.test", "correct-horse-battery-staple");
setup.EnsureSuccessStatusCode();
var login = await SetupHelper.LoginAsync(client, "ingestion-owner@itworx.test", "correct-horse-battery-staple");
login.EnsureSuccessStatusCode();
var session = await login.Content.ReadFromJsonAsync<SessionResponse>();
Assert.NotNull(session);
Assert.True(session.Authenticated);
Assert.NotNull(session.Features);
Assert.True(session.Features.IngestionProposals);
}
finally
{
await ((IAsyncLifetime)configuredFactory).DisposeAsync();
}
}
[Fact]
public async Task Ingestion_write_is_an_accepted_service_account_scope()
{
using var client = Factory.CreateClient();
var login = await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
login.EnsureSuccessStatusCode();
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/service-accounts",
new CreateServiceAccountRequest("ingestion", "trusted intake", [Scopes.IngestionWrite]));
response.EnsureSuccessStatusCode();
}
private static ClaimsPrincipal ServicePrincipal(string scope)
{
var identity = new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new Claim(ServiceTokenAuthenticationHandler.ServiceAccountClaimType, Guid.NewGuid().ToString()),
new Claim(ServiceTokenAuthenticationHandler.ScopeClaimType, scope)
], ServiceTokenAuthenticationDefaults.Scheme);
return new ClaimsPrincipal(identity);
}
private static ClaimsPrincipal HumanPrincipal(Guid userId) => new(new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Name, "human-editor")
], authenticationType: "cookie"));
}