Workflow-driven skill that plans and implements the cross-tenant leak-test suite - from surface inventory to the runtime CI gate a multi-tenant codebase must pass on every PR. The planning section inventories tenant-bearing surfaces (tables, APIs, object storage, search, queues, caches), classifies each by isolation model (silo / pool / bridge, per references/isolation-models.md), and derives the OWASP WSTG-ATHZ-02 coverage matrix. The battery defines the canonical test patterns (read-other-tenant-by-id, list-leak, spoofed-tenant-id-in-body, JWT-replay, FK-cross-tenant, unique-collision side channel, object-storage IDOR, search-index-direct-query, async-job-context-reload, cache-key-collision), the 404-vs-403 disclosure trade-off, the Postgres-RLS-direct patterns, and the CI integration (non-superuser non-BYPASSRLS role, fail the build on any leak). Use when designing or implementing a tenant-isolation test suite, adding the CI gate to an existing project, or investigating a leak finding.
90
90%
Does it follow best practices?
Impact
94%
1.05xAverage score across 3 eval scenarios
High
Do not use without reviewing
We are adding file attachments to a multi-tenant product. The feature is written and under review. Before it ships, the team wants a test plan for tenant isolation - what needs probing, and how.
Our last isolation review looked only at HTTP handlers and missed a bug where one tenant's uploads appeared in another tenant's search results.
Produce a file named attachment-isolation-test-plan.md containing:
Do not write test code and do not modify the source files. This is the plan the team will review before anyone implements it.
Extract the following files before beginning.
=============== FILE: src/attachments/routes.js =============== 'use strict';
const { store } = require('./storage'); const { indexer } = require('./search'); const { publish } = require('./events'); const { cache } = require('./cache');
async function uploadAttachment(session, file) {
const key = attachments/${session.tenantId}/${file.name};
await store.put(key, file.body);
const record = await db.insert('attachments', {
tenantId: session.tenantId,
key,
name: file.name,
uploadedBy: session.userId,
});
await indexer.add({ id: record.id, name: file.name, body: file.text });
await publish('attachment.created', { attachmentId: record.id });
return record;
}
async function getAttachment(session, id) {
const cached = await cache.get(attachment:${id});
if (cached) {
return cached;
}
const record = await db.findOne('attachments', { id, tenantId: session.tenantId });
if (!record) {
return null;
}
await cache.set(attachment:${id}, record);
return record;
}
async function searchAttachments(session, query) { return indexer.query(query); }
module.exports = { uploadAttachment, getAttachment, searchAttachments };
=============== FILE: src/attachments/storage.js =============== 'use strict';
const store = { async put(key, body) { return objectStore.write(key, body); }, async get(key) { return objectStore.read(key); }, async signedUrl(key, ttlSeconds) { return objectStore.sign(key, ttlSeconds); }, };
module.exports = { store };
=============== FILE: src/attachments/search.js =============== 'use strict';
const indexer = { async add(document) { return searchClient.index('attachments', document); }, async query(text) { return searchClient.search('attachments', { text }); }, };
module.exports = { indexer };
=============== FILE: src/attachments/events.js =============== 'use strict';
async function publish(topic, payload) { return broker.send(topic, payload); }
async function onAttachmentCreated(message) { const record = await db.findOne('attachments', { id: message.attachmentId }); await notify(record.uploadedBy, 'Your file is ready'); }
module.exports = { publish, onAttachmentCreated };
=============== FILE: src/attachments/cache.js =============== 'use strict';
const cache = { async get(key) { return redis.get(key); }, async set(key, value) { return redis.set(key, value, 'EX', 300); }, };
module.exports = { cache };