Files
qbit-authentik/src/test/java/com/kingsrook/qbits/authentik/admin/AuthentikInvitationEvidenceTest.java
T
james.maesandGPT-6 2e356b8117
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 38s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 38s
qbit release candidate / cut-rc (pull_request) Failing after 18s
Release candidate / release-rc (pull_request) Failing after 18s
qbit release candidate / tag-and-release (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Failing after 15m39s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Failing after 24s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Failing after 1h14m34s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 10s
Shared Java Maven CI with Docker / verify (push) Failing after 17m19s
Shared Java Maven CI with Docker / gate (push) Failing after 24s
qrun-release / release (push) Failing after 1h16m30s
qrun-release / postpublish (push) Skipped
feat(authentik): checkpoint application access and identity synchronization RC
Add user and group QQQ application-role access editing, bounded catalogs,
webhook inbox and SQL mirrors, refreshed session permissions, and deferred
invitation assignments. Include the isolated local review host, browser
acceptance scripts, documentation and the token-widget column mapping fix.

User approved this RC checkpoint. Full 1.0 gates remain documented, including
the schema-drift matcher and PostgreSQL integration errors from the full gate.
Companion UI: QRun-IO/qqq-frontend-next#205.

Co-Authored-By: GPT-6 <noreply@openai.com>
2026-11-05 05:29:54 -06:00

155 lines
8.0 KiB
Java

/*
* Copyright 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.authentik.admin;
import java.time.Instant;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qbits.authentik.AuthentikTestSupport;
import com.kingsrook.qbits.authentik.client.FixtureActionUtil;
import com.kingsrook.qbits.authentik.security.IdentityPermission;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** Native event facts fail closed on forged hints, wrong identities and edits.
*******************************************************************************/
class AuthentikInvitationEvidenceTest
{
private static final String EVENT = "61000000-0000-0000-0000-000000000001";
private static final String INVITATION = "62000000-0000-0000-0000-000000000002";
private static final String REFERENCE = "63000000-0000-0000-0000-000000000003";
private static final Instant START = Instant.parse("2026-11-05T00:00:00Z");
private FixtureActionUtil client;
private AuthentikInvitationEvidence evidence;
private JSONObject response;
/***************************************************************************
** Production guarded client with only HTTP transport replaced.
***************************************************************************/
@BeforeEach
void setup() throws Exception
{
FixtureActionUtil.reset();
var config = AuthentikTestSupport.unitConfig().withListCacheTtlSeconds(300);
var instance = AuthentikTestSupport.produce(config);
AuthentikTestSupport.loginWith(instance, IdentityPermission.AUDIT_READ);
client = new FixtureActionUtil();
client.setBackendMetaData((APIBackendMetaData) instance.getBackend(config.getApiBackendName()));
evidence = new AuthentikInvitationEvidence(client);
var configuration = AuthentikInvitationFlowTest.configuration();
FixtureActionUtil.handler = request ->
{
var body = configuration.getOrDefault(request.getURI().getPath(), response);
return (body == null ? FixtureActionUtil.response(404, "{}") : FixtureActionUtil.response(200, body.toString()));
};
}
/***************************************************************************
** No authenticated context or test handler escapes this fixture.
***************************************************************************/
@AfterEach
void cleanup()
{
QContext.clear();
FixtureActionUtil.reset();
}
/***************************************************************************
** Only native facts escape; arbitrary event permissions are never returned.
***************************************************************************/
@Test
void readsConsumptionAndNewUserFactsWithoutCaching() throws Exception
{
response = event("invitation_used");
response.getJSONObject("context").put("invitation_uuid", INVITATION.replace("-", ""));
assertThat(evidence.consumed(EVENT, INVITATION, "enroll", START, START.plusSeconds(60)).invitationId()).isEqualTo(INVITATION);
response = event("user_write");
response.getJSONObject("context").put("permissions", List.of("authentik.*"));
var enrolled = evidence.enrolled(EVENT, REFERENCE, "enroll", START, START.plusSeconds(60));
assertThat(enrolled.userPk()).isEqualTo(12);
assertThat(enrolled.reference()).isEqualTo(REFERENCE);
assertThat(enrolled.createdAt()).isEqualTo(START.plusSeconds(1));
response = null;
assertThatThrownBy(() -> evidence.enrolled(EVENT, REFERENCE, "enroll", START, START.plusSeconds(60))).isInstanceOf(QException.class);
}
/***************************************************************************
** Wrong source identity, action, flow, time, correlation and edited accounts
** must not be mistaken for the requested enrollment.
***************************************************************************/
@Test
void rejectsMismatchedAndAmbiguousEnrollment()
{
List<Consumer<JSONObject>> mutations = List.of(
value -> value.put("pk", INVITATION),
value -> value.put("action", "custom_user_write"),
value -> value.put("created", START.minusSeconds(1).toString()),
value -> value.put("created", START.plusSeconds(61).toString()),
value -> value.getJSONObject("context").getJSONObject("http_request").put("path", "/api/v3/flows/executor/other/"),
value -> value.getJSONObject("context").put(AuthentikInvitationEvidence.REFERENCE, INVITATION),
value -> value.getJSONObject("context").put("created", false),
value -> value.getJSONObject("context").put("created", "true"),
value -> value.getJSONObject("user").put("pk", 1.5),
value -> value.getJSONObject("user").put("pk", 0),
value -> value.getJSONObject("user").put("pk", "12"),
value -> value.getJSONObject("user").put("on_behalf_of", new JSONObject()),
value -> value.getJSONObject("user").put("authenticated_as", new JSONObject()),
value -> value.remove("context"));
for(var mutation : mutations)
{
response = event("user_write");
mutation.accept(response);
assertThatThrownBy(() -> evidence.enrolled(EVENT, REFERENCE, "enroll", START, START.plusSeconds(60)))
.isInstanceOf(QException.class).hasMessage("Invitation enrollment evidence could not be verified.").hasNoCause();
}
}
/***************************************************************************
** Consumption must identify the saved invitation, not the event's actor.
***************************************************************************/
@Test
void rejectsWrongConsumptionAndInvalidLookupIds()
{
response = event("invitation_used");
response.getJSONObject("context").put("invitation_uuid", REFERENCE);
assertThatThrownBy(() -> evidence.consumed(EVENT, INVITATION, "enroll", START, START.plusSeconds(60)))
.isInstanceOf(QException.class).hasNoCause();
int requests = FixtureActionUtil.REQUESTS.size();
assertThatThrownBy(() -> evidence.enrolled("1-1-1-1-1", REFERENCE, "enroll", START, START.plusSeconds(60)))
.isInstanceOf(QException.class);
assertThat(FixtureActionUtil.REQUESTS).hasSize(requests);
}
/***************************************************************************
** Direct client access checks audit permission before any network request.
***************************************************************************/
@Test
void directEventReadRetainsGuard() throws Exception
{
AuthentikTestSupport.loginWith(QContext.getQInstance(), IdentityPermission.USERS_READ);
assertThatThrownBy(() -> client.syncEvent(EVENT)).isInstanceOf(com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException.class);
assertThat(FixtureActionUtil.REQUESTS).isEmpty();
}
/***************************************************************************
** Synthetic form matching the pinned native event schema.
***************************************************************************/
private JSONObject event(String action)
{
return (new JSONObject().put("pk", EVENT).put("action", action).put("created", START.plusSeconds(1).toString())
.put("user", new JSONObject().put("pk", 12))
.put("context", new JSONObject().put("created", true).put(AuthentikInvitationEvidence.REFERENCE, REFERENCE)
.put("http_request", new JSONObject().put("path", "/api/v3/flows/executor/enroll/"))));
}
}