Public: anyone can read this; the crew can change it.

A site admin grants an app's private destinations on its settings page, and only the repository's admins hear about it #3686

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3621-a-site-admin-grants-an-app-s-private-des → main 10 commits fork 1 thread · 1 unresolved

  • Merged. scout/3621-a-site-admin-grants-an-app-s-private-des was merged into main as 66889d7.

Review

Sign in to review this change.
2 files changed +22 −0
src/core/event_audience.ts +2
@@ -139,6 +139,8 @@ export function classifiedAudience(kind: string): EventAudience | undefined {
139139 kind.startsWith("webhook.") ||
140140 kind.startsWith("app.env_") ||
141141 kind.startsWith("app.domain.") ||
142 // A private address and port, and who may reach it (#3543).
143 kind.startsWith("app.egress.") ||

Right place and right audience. The prefix covers both granted and revoked.

Right place and right audience. Follow-up: cover the HTTP outbox and tty feed with a test for this kind.

142144 kind.startsWith("prompt.") ||
143145 kind.startsWith("secret.") ||
144146 // The forge's mail ceiling is its owner's to hear (`actions/mail.ts`).
test/event-audience.test.ts +20
@@ -28,6 +28,8 @@ import { serve, type Servers } from "../src/http/server.ts";
2828
2929 const HOOK = "https://hooks.example.test/services/T000/B000/s3cr3t-in-the-path";
3030
31 const EGRESS = "100.89.95.68:8101";
32
3133 describe("who an event is for", () => {
3234 let dataDir: string;
3335 let app: App;
@@ -88,6 +90,12 @@ describe("who an event is for", () => {
8890 "sk_live_x",
8991 );
9092 if (!set.ok) throw new Error(String(set.error));
93 app.bus.append({
94 kind: "app.egress.granted",
95 repoId: repo.id,
96 actorId: alice.id,
97 payload: { destination: EGRESS },
98 });
9199 expect(pinRepo(app, bob, repo).ok).toBe(true);
92100 }, 30_000);
93101
@@ -108,6 +116,8 @@ describe("who an event is for", () => {
108116 expect(eventAudience("webhook.deleted")).toBe("admins");
109117 expect(eventAudience("app.env_set")).toBe("admins");
110118 expect(eventAudience("app.env_deleted")).toBe("admins");
119 expect(eventAudience("app.egress.granted")).toBe("admins");
120 expect(eventAudience("app.egress.revoked")).toBe("admins");
111121 expect(eventAudience("repo.settings_updated")).toBe("admins");
112122 expect(eventAudience("prompt.told")).toBe("admins");
113123 expect(eventAudience("prompt.source_added")).toBe("admins");
@@ -134,6 +144,16 @@ describe("who an event is for", () => {
134144 expect(alices).not.toContain("repo.pinned"); // bob's, even to an admin
135145 });
136146
147 test("a private destination and its granter are for the repository's admins", () => {
148 const all = listEvents(app.db, { repoId: repo.id });
149 const text = (viewer: User | undefined) =>
150 JSON.stringify(all.filter(audienceFilter(app.db, viewer)));
151 expect(text(undefined)).not.toContain(EGRESS);
152 expect(text(bob)).not.toContain(EGRESS);
153 expect(text(alice)).toContain(EGRESS);
154 expect(text(alice)).toContain(alice.id);
155 });
156
137157 test("the JSON outbox never tells a reader a webhook's URL or a variable's name", async () => {
138158 for (const headers of [{}, { authorization: `Bearer ${bobToken}` }]) {
139159 const text = await fetch(`${base}/api/v1/repos/alice/open/events`, {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs