fleetd #672: pin AuthorizationMode.ENFORCED at FleetdAssembly.java:481
CI / shell-tests (pull_request) Failing after 6s
CI / contract (pull_request) Successful in 1m6s
CI / build (pull_request) Failing after 1m41s

Adds FleetdAssemblyAuthorizationModeTest: drives FleetMcp#denyFor (via reflection, since it is package-private to dev.ltms.fleet.mcp) on the real FleetMcp FleetdAssembly#assembleAndStart builds, asserting an unauthorized worker is refused SPAWN and the primary is still allowed. Mutating line 481 to UNENFORCED turns this test red and no other test.
This commit is contained in:
Dai Ha
2026-10-03 20:43:21 +02:00
parent 7f9a9c09f9
commit 367facf6a6
@@ -0,0 +1,185 @@
package dev.ltms.fleet;
import dev.ltms.fleet.auth.Authz;
import dev.ltms.fleet.auth.Principal;
import dev.ltms.fleet.config.ConfigRef;
import dev.ltms.fleet.config.FleetConfig;
import dev.ltms.fleet.guard.SubscriptionGuard;
import dev.ltms.fleet.herdr.FakeHerdr;
import dev.ltms.fleet.herdr.HerdrClient;
import dev.ltms.fleet.mcp.FleetMcp;
import dev.ltms.fleet.msg.ReplyInbox;
import io.javalin.Javalin;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.LongSupplier;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* fleetd #672 — pins the {@link FleetMcp.AuthorizationMode} that {@link FleetdAssembly}'s
* production boot path passes to {@link FleetMcp} at {@code FleetdAssembly.java:481}
* ({@code AuthorizationMode.ENFORCED}).
*
* <p>{@code FleetMcpAuthzTest} already exercises {@code FleetMcp#denyFor} against the CB-505
* table, but it constructs its own {@link FleetMcp} and chooses its own {@code AuthorizationMode}
* — it tests the seam, not the producer. This test instead reaches the exact {@link FleetMcp}
* {@link FleetdAssembly#assembleAndStart} builds (via {@code FleetdRuntime#mcp()}, the same
* accessor {@code FleetdAssemblyConnectionIdentityTest} uses for {@code identity()}) and asserts
* the consequence rather than reading the enum back: an unauthorized caller must actually be
* refused through it, and the primary must still be allowed, so the test cannot pass with the
* gate wired backwards.
*
* <p>{@code FleetMcp#denyFor} is package-private to {@code dev.ltms.fleet.mcp}; this test lives in
* {@code dev.ltms.fleet}, where {@link FleetdAssembly} and {@code FleetdRuntime#mcp()} live, so it
* cannot call {@code denyFor} directly. Reflection bridges that package boundary the same way
* {@code getDeclaredField} does in {@link FleetdAssemblyLeadTabScannerExclusionTest} — the
* assertion itself still exercises the real policy decision ({@code denyFor} calling
* {@code Authz.permits}), not a field read.
*/
class FleetdAssemblyAuthorizationModeTest {
private static final class TestResourcePorts implements ResourcePorts {
final FakeHerdr herdr = new FakeHerdr();
Runnable shutdownHook;
@Override
public Map<String, String> environment() {
return Map.of();
}
@Override
public HerdrClient connectHerdr(Path socketPath) {
return herdr;
}
@Override
public Fleetd.AmqpOpener replyInboxOpener() {
return (uri, prefetch) -> new ReplyInbox() {
@Override public void own(String target) { }
@Override public void release(String target) { }
@Override public void publish(String target, String msgId, String content) { }
@Override public List<InboxMessage> peek(String target) { return List.of(); }
@Override public boolean ack(String target, String msgId) { return false; }
};
}
@Override
public Fleetd.LeadMailboxOpener leadMailboxOpener() {
return (uri, selfCoordId, prefetch) -> {
throw new UnsupportedOperationException(
"leadMailboxOpener must not be called — no coordinator: block is configured");
};
}
@Override
public LongSupplier nanoClock() {
return System::nanoTime;
}
@Override
public LongSupplier wallClockNanos() {
return System::nanoTime;
}
@Override
public ScheduledExecutorService newScheduler(String purpose) {
return Executors.newSingleThreadScheduledExecutor();
}
@Override
public void addShutdownHook(Runnable hook) {
shutdownHook = hook;
}
@Override
public void startHttp(Javalin app, String host, int port) {
// Do not bind a real port in this assembly test — see FleetdAssemblyLeadTabScannerExclusionTest.
}
@Override
public Runnable herdrPollWait() {
return () -> {
throw new UnsupportedOperationException("FakeHerdr is healthy; no poll wait is expected");
};
}
}
private TestResourcePorts ports;
@AfterEach
void tearDown() {
if (ports != null && ports.shutdownHook != null) {
ports.shutdownHook.run();
}
}
private static FleetConfig writeConfig(Path dir) throws Exception {
Path file = dir.resolve("fleetd.yaml");
Files.writeString(file, """
bind:
host: 127.0.0.1
port: 8765
idleSleepGuard:
enabled: false
health:
enabled: false
broker:
uri: "amqp://fake-test-broker/vh"
""");
return FleetConfig.load(file);
}
private FleetMcp assemble(Path dir) throws Exception {
FleetConfig cfg = writeConfig(dir);
ports = new TestResourcePorts();
FleetdRuntime runtime = FleetdAssembly.assembleAndStart(new AssemblyInputs(cfg,
new ConfigRef(dir.resolve("fleetd.yaml"), cfg), new SubscriptionGuard(cfg.guard().hostSet())), ports);
return runtime.mcp();
}
/**
* Invokes the real production {@code FleetMcp#denyFor} by reflection. The method is
* package-private to {@code dev.ltms.fleet.mcp}; this is the only seam available to this test
* without a full HTTP/servlet round trip (see this class's javadoc). No {@code catch} here can
* turn a missing method into a pass — a {@code NoSuchMethodException} propagates out of the
* test and fails it loudly if {@code denyFor} is ever renamed or removed.
*/
private static McpSchema.CallToolResult denyFor(FleetMcp mcp, Principal caller, Authz.Action action,
String target) throws Exception {
Method m = FleetMcp.class.getDeclaredMethod("denyFor", Principal.class, Authz.Action.class, String.class);
m.setAccessible(true);
return (McpSchema.CallToolResult) m.invoke(mcp, caller, action, target);
}
@Test
void productionBootPathRefusesAnUnauthorizedCallerThroughTheAssembledFleetMcp(@TempDir Path dir)
throws Exception {
FleetMcp mcp = assemble(dir);
McpSchema.CallToolResult deniedForWorker = denyFor(mcp, Principal.worker("term_a", 200),
Authz.Action.SPAWN, "term_a");
assertNotNull(deniedForWorker,
"FleetdAssembly.java:481 must pass AuthorizationMode.ENFORCED to FleetMcp — a worker "
+ "must not be able to fleet_spawn through the assembled production object");
assertTrue(deniedForWorker.isError(), "a refusal is returned as an MCP tool error");
McpSchema.CallToolResult allowedForPrimary = denyFor(mcp, Principal.primary(100),
Authz.Action.SPAWN, "term_a");
assertNull(allowedForPrimary,
"control: the primary must still be allowed to fleet_spawn — otherwise the worker "
+ "refusal above would pass even with the gate wired backwards");
}
}