Skip to content

Commit 2bc34da

Browse files
committed
fix(runtime): refuse the walled catalog discard-drafts door; judge duplicate by the rows it copies
POST /packages/:id/discard-drafts now asks refuseWalledCatalogPackageWrite with the package's draft rows, like /revert: the two verbs discard the same drafts. The duplicate door now reads exactly duplicatePackage's selection (active, organization-less rows). Pins: discard-drafts is refused under isolated and group (the draft stays); the operator, single and tenant catalog-free controls run it. The tenant control's title and the file header now name the verbs it covers (all but adopt-orphans). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
1 parent 0e4fdb4 commit 2bc34da

2 files changed

Lines changed: 35 additions & 8 deletions

File tree

‎packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@
1414
// undid the operator's published change: `200`, and B's member lost it;
1515
// - `POST /packages/:id/duplicate` copied the set, under the same name, into
1616
// A's own package, and `GET /meta` then served A's copy;
17-
// - `POST /packages/:id/revert` discarded the operator's pending draft;
17+
// - `POST /packages/:id/revert` discarded the operator's pending draft
18+
// (`POST /packages/:id/discard-drafts` is the same discard, by a second door);
1819
// - `POST /packages/:id/adopt-orphans` moved the environment's catalog rows
1920
// into A's package.
2021
//
2122
// Pinned here, under each walled posture: every one of those answers
2223
// `403 PERMISSION_DENIED`, nothing moves, and B's resolution stays. The commit
2324
// revert is judged by the commit's own items, whatever package the URL names.
24-
// CONTROLS: A runs every one of these doors on its own catalog-free package as
25-
// before, and the platform operator runs them on the catalog-carrying one.
26-
// And under `single` (CONTROL): the same `manage_metadata` holder runs them on
27-
// the operator's package as before.
25+
// CONTROLS: A runs publish-drafts, discard-drafts, revert, rollback, commit
26+
// revert and duplicate on its own catalog-free package as before. Not
27+
// adopt-orphans: that verb reads the environment's orphans, not A's package,
28+
// and this environment carries the operator's catalog orphan. The platform
29+
// operator runs every door on the catalog-carrying package, adopt-orphans
30+
// included. And under `single` (CONTROL): the same `manage_metadata` holder
31+
// runs every door on the operator's package as before.
2832
//
2933
// The boot is `multiTenant: 'posture-only'`: a real, non-degraded walled
3034
// posture. The harness declares the seeded admin as `OS_PLATFORM_OWNER_EMAIL`,
@@ -199,6 +203,14 @@ for (const posture of ['isolated', 'group'] as const) {
199203
expect(await rowsOf(h, { package_id: OP_PKG, type: 'permission', name: OP_SET, state: 'draft' })).toHaveLength(1);
200204
});
201205

206+
it('discard-drafts: A discarding the operator\'s pending catalog draft is refused, and the draft stays', async () => {
207+
const drafted = await call(h.stack, h.operator, 'PUT', `/meta/permission/${OP_SET}?package=${OP_PKG}&mode=draft`,
208+
set(OP_SET, ['manage_users', 'manage_metadata']));
209+
expect(drafted.status, await drafted.clone().text()).toBe(200);
210+
await refused(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/discard-drafts`, {}));
211+
expect(await rowsOf(h, { package_id: OP_PKG, type: 'permission', name: OP_SET, state: 'draft' })).toHaveLength(1);
212+
});
213+
202214
it('rollback: A rolling the operator\'s package back past a catalog commit is refused, and B keeps `manage_users`', async () => {
203215
const before = await bSnapshot();
204216
await refused(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/rollback`, { commitId: h.c1 }));
@@ -232,7 +244,7 @@ for (const posture of ['isolated', 'group'] as const) {
232244
expect(env.map((r) => r.package_id ?? null)).not.toContain('com.w3.a.adopt');
233245
});
234246

235-
it('CONTROL: A runs every one of these doors on its own catalog-free package as before', async () => {
247+
it('CONTROL: A runs every door but adopt-orphans on its own catalog-free package as before', async () => {
236248
const pkg = 'com.w3.a.free';
237249
const dash = 'w3_free_dash';
238250
const ok = async (res: Response) => expect(res.status, await res.clone().text()).toBe(200);
@@ -247,13 +259,19 @@ for (const posture of ['isolated', 'group'] as const) {
247259
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${pkg}/rollback`, { commitId: first }));
248260
await ok(await call(h.stack, h.tokA, 'PUT', `/meta/dashboard/${dash}?package=${pkg}&mode=draft`, dashboard(dash, 'v3')));
249261
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${pkg}/revert`, {}));
262+
await ok(await call(h.stack, h.tokA, 'PUT', `/meta/dashboard/${dash}?package=${pkg}&mode=draft`, dashboard(dash, 'v4')));
263+
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${pkg}/discard-drafts`, {}));
264+
expect(await rowsOf(h, { package_id: pkg, state: 'draft' })).toHaveLength(0);
250265
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${pkg}/duplicate`, { targetPackageId: 'com.w3.a.free.copy' }));
251266
});
252267

253268
it('CONTROL: the platform operator runs every one of these doors on its catalog-carrying package', async () => {
254269
const ok = async (res: Response) => expect(res.status, await res.clone().text()).toBe(200);
255270
await ok(await call(h.stack, h.operator, 'POST', `/packages/${OP_PKG}/duplicate`, { targetPackageId: 'com.w3.op.copy' }));
256271
await ok(await call(h.stack, h.operator, 'POST', `/packages/${OP_PKG}/revert`, {}));
272+
await ok(await call(h.stack, h.operator, 'PUT', `/meta/permission/${OP_SET}?package=${OP_PKG}&mode=draft`, set(OP_SET, ['manage_metadata'])));
273+
await ok(await call(h.stack, h.operator, 'POST', `/packages/${OP_PKG}/discard-drafts`, {}));
274+
expect(await rowsOf(h, { package_id: OP_PKG, type: 'permission', name: OP_SET, state: 'draft' })).toHaveLength(0);
257275
await ok(await call(h.stack, h.operator, 'POST', `/packages/${OP_PKG}/commits/${h.c2}/revert`, {}));
258276
expect((await bGrants()).systemPermissions).not.toContain('manage_users');
259277
await ok(await call(h.stack, h.operator, 'PUT', `/meta/permission/${OP_SET}?package=${OP_PKG}&mode=draft`, set(OP_SET, ['manage_users'])));
@@ -289,6 +307,8 @@ describe('[ADR-0131 D3 · #15204 W3] single (CONTROL): the refusal is inert', ()
289307
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/rollback`, { commitId: h.c1 }));
290308
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/commits/${h.c1}/revert`, {}));
291309
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/revert`, {}));
310+
await ok(await call(h.stack, h.operator, 'PUT', `/meta/permission/${OP_SET}?package=${OP_PKG}&mode=draft`, set(OP_SET, ['manage_metadata'])));
311+
await ok(await call(h.stack, h.tokA, 'POST', `/packages/${OP_PKG}/discard-drafts`, {}));
292312
expect((await call(h.stack, h.tokA, 'POST', '/packages', { id: 'com.w3.a.adopt', name: 'adopt', version: '1.0.0', type: 'app' })).status).toBe(201);
293313
await ok(await call(h.stack, h.tokA, 'POST', '/packages/com.w3.a.adopt/adopt-orphans', {}));
294314
});

‎packages/runtime/src/domains/packages.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,13 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin
18661866
if (parts.length === 2 && parts[1] === 'discard-drafts' && m === 'POST') {
18671867
const denied = requireManageMetadata(deps, _context); if (denied) return denied;
18681868
const id = decodeURIComponent(parts[0]);
1869+
// [#15204 W3] ADR-0131 D3: discarding a catalog draft is the
1870+
// operator's under a wall. The drafts this verb discards.
1871+
const walled = await refuseWalledCatalogPackageWrite(
1872+
deps, _context, `Discarding the drafts of package "${id}"`,
1873+
storedRowTypes(qlService, { package_id: id, state: 'draft' }),
1874+
);
1875+
if (walled) return walled;
18691876
const protocol = await resolveProtocol(deps, _context);
18701877
if (protocol && typeof protocol.discardPackageDrafts === 'function') {
18711878
try {
@@ -2101,10 +2108,10 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin
21012108
// [#15204 W3] ADR-0131 D3: a duplicate copies the source's items
21022109
// into a new package, and a copied catalog item is a catalog
21032110
// creation (its name is not re-namespaced), the operator's under
2104-
// a wall. The source's stored rows.
2111+
// a wall. The rows `duplicatePackage` copies: active, environment-wide.
21052112
const walled = await refuseWalledCatalogPackageWrite(
21062113
deps, _context, `Duplicating package "${id}"`,
2107-
storedRowTypes(qlService, { package_id: id }),
2114+
storedRowTypes(qlService, { package_id: id, state: 'active', organization_id: null }),
21082115
);
21092116
if (walled) return walled;
21102117
try {

0 commit comments

Comments
 (0)