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; /// /// 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. /// [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(); 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(); 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(); 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>().Value; Assert.False(options.Enabled); using var client = Factory.CreateClient(); var session = await client.GetFromJsonAsync("/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(); 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(); Assert.NotNull(session?.UserId); Assert.Equal("Editor", session.Role); using var scope = Factory.Services.CreateScope(); var accessor = scope.ServiceProvider.GetRequiredService(); accessor.HttpContext = new DefaultHttpContext { User = HumanPrincipal(session.UserId.Value) }; try { var authorization = scope.ServiceProvider.GetRequiredService(); 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 { ["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(); 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")); }