219 lines
9.2 KiB
C#
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"));
|
|
}
|