125 lines
5.2 KiB
Java
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));
|
|
}
|
|
}
|