diff --git a/packages/server/src/server/hub/relationship-controller.test.ts b/packages/server/src/server/hub/relationship-controller.test.ts index 376b4585ec..55c35db7ff 100644 --- a/packages/server/src/server/hub/relationship-controller.test.ts +++ b/packages/server/src/server/hub/relationship-controller.test.ts @@ -116,6 +116,29 @@ describe("Hub relationship", () => { }); }); + test("Hub enrollment cannot widen the daemon's locally granted scopes", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.returnEnrollmentScopes(["hub.execution.*", "*"]); + + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + const responses = relationship.sendHubRequestOnLatest({ + type: "daemon.get_status.request", + requestId: "scope-escalation", + }); + + expect(relationship.relationshipFile()?.relationship.scopes).toEqual(["hub.execution.*"]); + expect(responses).toContainEqual({ + type: "rpc_error", + payload: { + requestId: "scope-escalation", + requestType: "daemon.get_status.request", + error: "Session is not authorized for daemon.get_status.request", + code: "access_denied", + }, + }); + }); + test("a lost enrollment response reuses the exact ceremony", async () => { relationship = await HubRelationshipHarness.start(); relationship.holdEnrollment(); diff --git a/packages/server/src/server/hub/relationship-controller.ts b/packages/server/src/server/hub/relationship-controller.ts index 6ac3fb3d9a..117d644388 100644 --- a/packages/server/src/server/hub/relationship-controller.ts +++ b/packages/server/src/server/hub/relationship-controller.ts @@ -343,7 +343,7 @@ export class HubRelationshipController implements HubRelationshipManagement { const active: ActiveRecord = { version: 1, state: "active", - relationship: { ...pending.relationship, scopes: enrollment.scopes }, + relationship: pending.relationship, credential: pending.credential, transport: { kind: "direct_websocket", webSocketUrl: enrollment.webSocketUrl }, }; diff --git a/packages/server/src/server/hub/test-utils/relationship-harness.ts b/packages/server/src/server/hub/test-utils/relationship-harness.ts index 8b262be4c1..1b6a45f82f 100644 --- a/packages/server/src/server/hub/test-utils/relationship-harness.ts +++ b/packages/server/src/server/hub/test-utils/relationship-harness.ts @@ -318,6 +318,7 @@ class InMemoryHubRelationships implements HubRelationshipRemote { private enrollmentObserved = deferred(); private socketObserved = deferred(); private enrollmentRejection: 401 | 403 | null = null; + private enrollmentScopes = ["hub.execution.*"]; private revokeFailures = 0; private readonly relationships = new Set(); readonly enrollmentSnapshots: RelationshipInvocationSnapshot[] = []; @@ -333,6 +334,10 @@ class InMemoryHubRelationships implements HubRelationshipRemote { this.enrollmentRejection = statusCode; } + returnEnrollmentScopes(scopes: string[]): void { + this.enrollmentScopes = scopes.slice(); + } + async enroll(input: HubEnrollment): Promise { this.enrollmentSnapshots.push(this.captureRelationship()); this.enrollments.push({ ...input, scopes: input.scopes.slice() }); @@ -415,7 +420,7 @@ class InMemoryHubRelationships implements HubRelationshipRemote { private enrollmentResult(input: HubEnrollment): HubEnrollmentResult { return { daemonId: input.daemonId, - scopes: ["hub.execution.*"], + scopes: this.enrollmentScopes.slice(), webSocketUrl: SOCKET_URL, }; } @@ -579,6 +584,10 @@ export class HubRelationshipHarness { this.remote.completeEnrollment(); } + returnEnrollmentScopes(scopes: string[]): void { + this.remote.returnEnrollmentScopes(scopes); + } + loseEnrollmentResponse(): void { this.remote.loseEnrollmentResponse(); }