Files
fleetd/bridged/src/test/java/dev/ltms/bridged/auth/AuthzTest.java
T
Dai Ha 21cfc09f8e
CI / build (pull_request) Successful in 55s
CI / contract (pull_request) Successful in 1m6s
CB-548: config-declared architect slots + Role.ARCHITECT authz
2026-08-13 17:28:14 +02:00

125 lines
5.2 KiB
Java

package dev.ltms.bridged.auth;
import org.junit.jupiter.api.Test;
import static dev.ltms.bridged.auth.Authz.Action.*;
import static org.junit.jupiter.api.Assertions.*;
/** CB-505 — the authorization table, pinned so it cannot drift silently. */
class AuthzTest {
private static final Principal PRIMARY = Principal.primary(100);
private static final Principal WORKER_A = Principal.worker("term_a", 200);
private static final Principal WORKER_B = Principal.worker("term_b", 300);
private static final Principal ANON = Principal.anonymous();
private static final Principal ARCH_DESIGN = Principal.architect("lead-designer", "term_design", 400);
private static final Principal ARCH_OTHER = Principal.architect("reviewer", "term_review", 500);
@Test
void anonymousIsAuthorizedForNothing() {
for (Authz.Action a : Authz.Action.values()) {
assertFalse(Authz.permits(ANON, a, "term_a"),
a + " must be refused to an unauthenticated caller");
}
}
@Test
void aNullCallerIsTreatedAsAnonymous() {
assertFalse(Authz.permits(null, READ, null));
assertTrue(Authz.isUnauthenticated(null));
}
@Test
void orchestrationBelongsToThePrimaryAlone() {
for (Authz.Action a : new Authz.Action[]{SPAWN, STOP, SEND, DRAIN}) {
assertTrue(Authz.permits(PRIMARY, a, "term_a"), "the primary orchestrates: " + a);
assertFalse(Authz.permits(WORKER_A, a, "term_a"),
"a worker performing " + a + " would be escalating into the orchestrator role");
}
}
@Test
void aWorkerMayReplyAndAskOnlyAsItself() {
assertTrue(Authz.permits(WORKER_A, REPLY, "term_a"));
assertTrue(Authz.permits(WORKER_A, ASK, "term_a"));
assertFalse(Authz.permits(WORKER_A, REPLY, "term_b"),
"worker A must not be able to reply on worker B's session");
assertFalse(Authz.permits(WORKER_B, ASK, "term_a"),
"worker B must not be able to ask as worker A");
}
@Test
void thePrimaryMayNotForgeAWorkersReply() {
// Not a hypothetical nicety: a forged reply would resolve the rendezvous the primary is
// itself blocked on, corrupting the correlation between a turn and its answer.
assertFalse(Authz.permits(PRIMARY, REPLY, "term_a"));
assertFalse(Authz.permits(PRIMARY, ASK, "term_a"));
}
@Test
void aWorkerWithNoTargetCannotReply() {
assertFalse(Authz.permits(WORKER_A, REPLY, null),
"an absent session id must not satisfy the own-session rule");
}
// ── CB-548: the architect matrix ───────────────────────────────────────────────────────────
@Test
void anArchitectMaySendButNotSpawnStopOrDrain() {
assertTrue(Authz.permits(ARCH_DESIGN, SEND, "term_worker"),
"delegating a turn to a worker IS the architect's job");
assertTrue(Authz.permits(ARCH_DESIGN, SEND, null));
for (Authz.Action a : new Authz.Action[]{SPAWN, STOP, DRAIN}) {
assertFalse(Authz.permits(ARCH_DESIGN, a, null),
"an architect must not " + a + " — fleet lifecycle is the primary's alone, so "
+ "a coordinator cannot also stand up or tear down the fleet");
}
}
@Test
void anArchitectMayReplyAndAskOnlyAsItsOwnPane() {
assertTrue(Authz.permits(ARCH_DESIGN, REPLY, "term_design"), "its own pane is its own");
assertTrue(Authz.permits(ARCH_DESIGN, ASK, "term_design"));
assertFalse(Authz.permits(ARCH_DESIGN, REPLY, "term_review"),
"architect 'lead-designer' must not reply on reviewer's pane");
assertFalse(Authz.permits(ARCH_OTHER, ASK, "term_design"),
"reviewer must not ask as lead-designer — no terminal is another's");
assertFalse(Authz.permits(ARCH_DESIGN, REPLY, null),
"an absent target must not pass the own-session rule");
}
@Test
void anArchitectMayReadAndScrapeMetrics() {
assertTrue(Authz.permits(ARCH_DESIGN, READ, null));
assertTrue(Authz.permits(ARCH_DESIGN, METRICS, null));
}
@Test
void anArchitectIsNotCountedAsPrimaryOrWorker() {
assertFalse(Authz.permits(ARCH_DESIGN, SPAWN, null), "not a primary — no lifecycle");
assertFalse(ARCH_DESIGN.isPrimary());
assertFalse(ARCH_DESIGN.isWorker(), "an architect is its own role, not a widened worker");
assertTrue(ARCH_DESIGN.isArchitect());
}
@Test
void observationIsOpenToBothAuthenticatedRoles() {
assertTrue(Authz.permits(PRIMARY, READ, null));
assertTrue(Authz.permits(WORKER_A, READ, null));
assertTrue(Authz.permits(PRIMARY, METRICS, null));
assertTrue(Authz.permits(WORKER_A, METRICS, null));
}
@Test
void unauthenticatedIsDistinguishedFromMerelyForbidden() {
// Drives the 401-vs-403 split: a missing credential is fixable by the caller, a wrong role
// is not.
assertTrue(Authz.isUnauthenticated(ANON));
assertFalse(Authz.isUnauthenticated(WORKER_A));
assertFalse(Authz.isUnauthenticated(PRIMARY));
}
}