CB-561: require member registry for architects
This commit is contained in:
@@ -65,13 +65,13 @@ public final class CallerResolver {
|
|||||||
private final Function<String, MemberRole> memberSlotRoles;
|
private final Function<String, MemberRole> memberSlotRoles;
|
||||||
private final Function<String, String> memberSlotNames;
|
private final Function<String, String> memberSlotNames;
|
||||||
|
|
||||||
/** Loopback-trust resolver: no token required, historical behaviour. */
|
/** Loopback-trust resolver: no token required, historical behaviour. Test-only. */
|
||||||
public CallerResolver(ConnectionIdentity identity) {
|
CallerResolver(ConnectionIdentity identity) {
|
||||||
this(identity, false, null, Map.of());
|
this(identity, false, null, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** As {@link #CallerResolver(ConnectionIdentity, boolean, String, Map)} with no leads pinned. */
|
/** As {@link #CallerResolver(ConnectionIdentity, boolean, String, Map)} with no leads pinned. Test-only. */
|
||||||
public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token) {
|
CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token) {
|
||||||
this(identity, tokenMode, token, Map.of());
|
this(identity, tokenMode, token, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ public final class CallerResolver {
|
|||||||
* @param pinnedPrimaryTerminal the primary's own herdr {@code terminal_id}
|
* @param pinnedPrimaryTerminal the primary's own herdr {@code terminal_id}
|
||||||
* ({@code null}/blank = unpinned)
|
* ({@code null}/blank = unpinned)
|
||||||
*/
|
*/
|
||||||
public static CallerResolver pinnedTo(ConnectionIdentity identity, boolean tokenMode,
|
static CallerResolver pinnedTo(ConnectionIdentity identity, boolean tokenMode,
|
||||||
String token, String pinnedPrimaryTerminal) {
|
String token, String pinnedPrimaryTerminal) {
|
||||||
return new CallerResolver(identity, tokenMode, token,
|
return new CallerResolver(identity, tokenMode, token,
|
||||||
pinnedPrimaryTerminal == null || pinnedPrimaryTerminal.isBlank()
|
pinnedPrimaryTerminal == null || pinnedPrimaryTerminal.isBlank()
|
||||||
? Map.of() : Map.of(pinnedPrimaryTerminal, "primary"));
|
? Map.of() : Map.of(pinnedPrimaryTerminal, "primary"));
|
||||||
@@ -102,21 +102,11 @@ public final class CallerResolver {
|
|||||||
* {@link Role#PRIMARY} — rather than a worker. Empty = nothing pinned,
|
* {@link Role#PRIMARY} — rather than a worker. Empty = nothing pinned,
|
||||||
* so every pane resolves as a worker.
|
* so every pane resolves as a worker.
|
||||||
*/
|
*/
|
||||||
public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token,
|
CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token,
|
||||||
Map<String, String> leadTerminals) {
|
Map<String, String> leadTerminals) {
|
||||||
this(identity, tokenMode, token, fixed(leadTerminals), null);
|
this(identity, tokenMode, token, fixed(leadTerminals), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map-form of both registries (CB-548): lead terminals and the initial architect terminal
|
|
||||||
* bindings, each snapshotted at construction (a handed-over map is not offered as live state).
|
|
||||||
*/
|
|
||||||
public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token,
|
|
||||||
Map<String, String> leadTerminals,
|
|
||||||
Map<String, String> architectTerminals) {
|
|
||||||
this(identity, tokenMode, token, fixed(leadTerminals), fixed(architectTerminals));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live-registry form: {@code leadTerminals} is consulted on every resolve, so leads discovered
|
* Live-registry form: {@code leadTerminals} is consulted on every resolve, so leads discovered
|
||||||
* after startup (CB-531's tab scan) take effect without a restart.
|
* after startup (CB-531's tab scan) take effect without a restart.
|
||||||
@@ -125,29 +115,18 @@ public final class CallerResolver {
|
|||||||
* {@link #pinnedTo}: {@code Map} and {@code Supplier} overloads are ambiguous for a literal
|
* {@link #pinnedTo}: {@code Map} and {@code Supplier} overloads are ambiguous for a literal
|
||||||
* {@code null}.
|
* {@code null}.
|
||||||
*/
|
*/
|
||||||
public static CallerResolver withLeads(ConnectionIdentity identity, boolean tokenMode,
|
static CallerResolver withLeads(ConnectionIdentity identity, boolean tokenMode,
|
||||||
String token,
|
String token,
|
||||||
Supplier<Map<String, String>> leadTerminals) {
|
Supplier<Map<String, String>> leadTerminals) {
|
||||||
return new CallerResolver(identity, tokenMode, token, leadTerminals, null);
|
return new CallerResolver(identity, tokenMode, token, leadTerminals, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live-registry form for both {@code leadTerminals} and the CB-548 architect registry: both
|
* Live registry form that can confirm a bound slot is an architect slot.
|
||||||
* are consulted on every resolve, so a slot binding injected after startup takes effect
|
|
||||||
* without a restart.
|
|
||||||
*
|
*
|
||||||
* <p>A static factory rather than a constructor overload, for the same reason as
|
* <p>This is the only public construction path. It keeps terminal bindings and slot roles in
|
||||||
* {@link #pinnedTo}: too many {@code Map}/{@code Supplier} combinations to make {@code null}
|
* the same {@link MemberRegistry}, so a configured architect can resolve as an architect.
|
||||||
* unambiguous.
|
|
||||||
*/
|
*/
|
||||||
public static CallerResolver withLeadsAndMembers(ConnectionIdentity identity,
|
|
||||||
boolean tokenMode, String token,
|
|
||||||
Supplier<Map<String, String>> leadTerminals,
|
|
||||||
Supplier<Map<String, String>> architectTerminals) {
|
|
||||||
return new CallerResolver(identity, tokenMode, token, leadTerminals, architectTerminals);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Live registry form that can confirm a bound slot is an architect slot. */
|
|
||||||
public static CallerResolver withLeadsAndMembers(ConnectionIdentity identity,
|
public static CallerResolver withLeadsAndMembers(ConnectionIdentity identity,
|
||||||
boolean tokenMode, String token,
|
boolean tokenMode, String token,
|
||||||
Supplier<Map<String, String>> leadTerminals,
|
Supplier<Map<String, String>> leadTerminals,
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ class CallerResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void aBoundArchitectPaneResolvesToArchitectBeforeTheWorkerFallback() {
|
void aBoundArchitectPaneResolvesToArchitectBeforeTheWorkerFallback() {
|
||||||
|
// This is the production construction path used by Bridged.
|
||||||
Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null,
|
Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null,
|
||||||
Map::of, boundMembers("architect:lead-designer", MemberRole.ARCHITECT))
|
Map::of, boundMembers("architect:lead-designer", MemberRole.ARCHITECT))
|
||||||
.resolve("127.0.0.1", 42, null);
|
.resolve("127.0.0.1", 42, null);
|
||||||
@@ -338,9 +339,10 @@ class CallerResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void anUnboundPaneStillResolvesAsAWorker() {
|
void anUnboundPaneStillResolvesAsAWorker() {
|
||||||
Map<String, String> arch = Map.of("term_elsewhere", "reviewer");
|
MemberRegistry members = new MemberRegistry(new BridgedConfig.Fleet(Map.of(),
|
||||||
|
Map.of("lead-designer", new BridgedConfig.Slot("sonnet")), Map.of(), Map.of(), null));
|
||||||
Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null,
|
Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null,
|
||||||
Map::of, () -> arch).resolve("127.0.0.1", 42, null);
|
Map::of, members).resolve("127.0.0.1", 42, null);
|
||||||
|
|
||||||
assertEquals(Role.WORKER, p.role());
|
assertEquals(Role.WORKER, p.role());
|
||||||
assertNull(p.name());
|
assertNull(p.name());
|
||||||
@@ -376,16 +378,6 @@ class CallerResolverTest {
|
|||||||
assertEquals("architect:lead-designer", r.members().get("term_a"));
|
assertEquals("architect:lead-designer", r.members().get("term_a"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void theArchitectMapFormIsCopiedSoLaterMutationCannotGrantArchitect() {
|
|
||||||
Map<String, String> mutable = new java.util.LinkedHashMap<>();
|
|
||||||
CallerResolver r = new CallerResolver(workerIdentity(), false, null, Map.of(), mutable);
|
|
||||||
|
|
||||||
mutable.put("term_a", "sneaky");
|
|
||||||
|
|
||||||
assertEquals(Role.WORKER, r.resolve("127.0.0.1", 42, null).role());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void describeNamesTheArchitectSlot() {
|
void describeNamesTheArchitectSlot() {
|
||||||
assertEquals("architect:lead-designer",
|
assertEquals("architect:lead-designer",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dev.ltms.bridged.mcp;
|
|||||||
|
|
||||||
import dev.ltms.bridged.auth.Authz;
|
import dev.ltms.bridged.auth.Authz;
|
||||||
import dev.ltms.bridged.auth.CallerResolver;
|
import dev.ltms.bridged.auth.CallerResolver;
|
||||||
|
import dev.ltms.bridged.auth.MemberRegistry;
|
||||||
import dev.ltms.bridged.auth.Principal;
|
import dev.ltms.bridged.auth.Principal;
|
||||||
import dev.ltms.bridged.auth.Role;
|
import dev.ltms.bridged.auth.Role;
|
||||||
import dev.ltms.bridged.config.BridgedConfig;
|
import dev.ltms.bridged.config.BridgedConfig;
|
||||||
@@ -69,7 +70,8 @@ class BridgeMcpAuthzTest {
|
|||||||
|
|
||||||
mcp = new BridgeMcp(messages, workers, sessions, identity, sessions.asPresence(),
|
mcp = new BridgeMcp(messages, workers, sessions, identity, sessions.asPresence(),
|
||||||
new PrimaryRegistry(null),
|
new PrimaryRegistry(null),
|
||||||
enforce ? new CallerResolver(identity) : null,
|
enforce ? CallerResolver.withLeadsAndMembers(identity, false, null,
|
||||||
|
Map::of, new MemberRegistry(null)) : null,
|
||||||
metrics);
|
metrics);
|
||||||
return mcp;
|
return mcp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.ltms.bridged.rest;
|
package dev.ltms.bridged.rest;
|
||||||
|
|
||||||
import dev.ltms.bridged.auth.CallerResolver;
|
import dev.ltms.bridged.auth.CallerResolver;
|
||||||
|
import dev.ltms.bridged.auth.MemberRegistry;
|
||||||
import dev.ltms.bridged.config.BridgedConfig;
|
import dev.ltms.bridged.config.BridgedConfig;
|
||||||
import dev.ltms.bridged.guard.SubscriptionGuard;
|
import dev.ltms.bridged.guard.SubscriptionGuard;
|
||||||
import dev.ltms.bridged.herdr.AgentControl;
|
import dev.ltms.bridged.herdr.AgentControl;
|
||||||
@@ -65,9 +66,8 @@ class BridgedAppAuthTest {
|
|||||||
MessageService messages = new MessageService(agents, injector, new Rendezvous());
|
MessageService messages = new MessageService(agents, injector, new Rendezvous());
|
||||||
|
|
||||||
ConnectionIdentity identity = new ConnectionIdentity(new PaneLocator(herdr), _ -> pid);
|
ConnectionIdentity identity = new ConnectionIdentity(new PaneLocator(herdr), _ -> pid);
|
||||||
CallerResolver callers = tokenMode
|
CallerResolver callers = CallerResolver.withLeadsAndMembers(identity, tokenMode, token,
|
||||||
? new CallerResolver(identity, true, token)
|
Map::of, new MemberRegistry(null));
|
||||||
: new CallerResolver(identity);
|
|
||||||
metrics = BridgedMetrics.create(sessions, new dev.ltms.bridged.msg.InMemoryReplyInbox());
|
metrics = BridgedMetrics.create(sessions, new dev.ltms.bridged.msg.InMemoryReplyInbox());
|
||||||
|
|
||||||
app = new BridgedApp(herdr, workers, sessions, messages, sessions.asPresence(), null,
|
app = new BridgedApp(herdr, workers, sessions, messages, sessions.asPresence(), null,
|
||||||
|
|||||||
Reference in New Issue
Block a user