Skip to content

Commit 3892c3a

Browse files
committed
updates
1 parent cf7489f commit 3892c3a

12 files changed

Lines changed: 224 additions & 29 deletions

File tree

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id,first_name,last_name,date_of_birth,sex,mrn
2+
11111111-1111-4111-8111-111111111111,Alice,Anderson,1980-01-01,F,MRN-0001
3+
22222222-2222-4222-8222-222222222222,Bob,Brown,1985-05-05,M,MRN-0002
4+
99999999-9999-4999-8999-999999999999,Mallory,Malloy,1990-10-10,F,MRN-0003
5+
33333333-3333-4333-8333-333333333333,Carol,Chen,1975-03-15,F,MRN-0004
6+
44444444-4444-4444-8444-444444444444,Dave,Davis,1988-08-22,M,MRN-0005
7+
55555555-5555-4555-8555-555555555555,Eve,Evans,1992-11-30,F,MRN-0006
8+
66666666-6666-4666-8666-666666666666,Frank,Foster,1970-07-04,M,MRN-0007
9+
77777777-7777-4777-8777-777777777777,Grace,Gomez,1995-02-14,F,MRN-0008
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Adversarial RLS tests for prescriptions.
3+
*
4+
* Actors: Alice (legit), Bob (legit), Mallory (attacker).
5+
*
6+
* Patients come from __fixtures__/patients.csv via `pg.loadCsv` — change the CSV
7+
* to add more patients, no test code changes required. Prescriptions are seeded
8+
* inline by joining the medications catalog the module already ships.
9+
*
10+
* Toggle `currentUser` below and re-run to watch RLS gate different identities.
11+
*/
12+
import * as path from 'path';
13+
import { getConnections, PgTestClient, seed } from 'pgsql-test';
14+
15+
let pg: PgTestClient;
16+
let db: PgTestClient;
17+
let teardown: () => Promise<void>;
18+
19+
// Deterministic UUIDs keep CSV rows and test assertions in lock-step.
20+
const alice = {
21+
id: '11111111-1111-4111-8111-111111111111',
22+
role: 'patient' as const,
23+
};
24+
const bob = {
25+
id: '22222222-2222-4222-8222-222222222222',
26+
role: 'patient' as const,
27+
};
28+
const mallory = {
29+
id: '99999999-9999-4999-8999-999999999999',
30+
role: 'patient' as const,
31+
};
32+
33+
// ═══════════════════════════════════════════════════════════════════════════
34+
// TOGGLE ME — swap currentUser between alice / bob / mallory and re-run.
35+
// alice → the "currentUser sees Alice's scripts" test passes.
36+
// bob → that same test fails (RLS hides Alice's rows).
37+
// mallory → same test fails (RLS hides them from her too).
38+
//
39+
// The two Mallory-specific tests below pass regardless of this toggle.
40+
// ═══════════════════════════════════════════════════════════════════════════
41+
const currentUser = alice;
42+
// const currentUser = bob;
43+
// const currentUser = mallory;
44+
45+
beforeAll(async () => {
46+
({ pg, db, teardown } = await getConnections({}, [seed.pgpm()]));
47+
48+
// authenticated role is created by the patients module; grant membership
49+
// to the app_user that pgsql-test created before the seed.
50+
await pg.query(`GRANT authenticated TO app_user`);
51+
52+
// Bulk-load patients from CSV. COPY bypasses RLS — safe via `pg` (superuser).
53+
await pg.loadCsv({
54+
'patients.patients': path.join(__dirname, '__fixtures__/patients.csv'),
55+
});
56+
57+
// Seed prescriptions by joining to the pre-seeded medications catalog.
58+
// Alice → 2 scripts (amoxicillin, atorvastatin)
59+
// Bob → 1 script (metformin)
60+
// Mallory → 0 scripts — she's the attacker with no legitimate history.
61+
await pg.query(
62+
`INSERT INTO prescriptions.prescriptions
63+
(patient_id, medication_id, dosage, route, frequency, quantity, refills)
64+
SELECT $1::uuid, id, '500 mg', 'oral', 'TID x 7d', 21, 0
65+
FROM medications.medications WHERE generic_name = 'amoxicillin'
66+
UNION ALL
67+
SELECT $1::uuid, id, '20 mg', 'oral', 'QD', 30, 3
68+
FROM medications.medications WHERE generic_name = 'atorvastatin'
69+
UNION ALL
70+
SELECT $2::uuid, id, '500 mg', 'oral', 'BID', 60, 2
71+
FROM medications.medications WHERE generic_name = 'metformin';`,
72+
[alice.id, bob.id],
73+
);
74+
});
75+
76+
afterAll(async () => {
77+
await teardown();
78+
});
79+
80+
beforeEach(async () => {
81+
await pg.beforeEach();
82+
await db.beforeEach();
83+
});
84+
85+
afterEach(async () => {
86+
await db.afterEach();
87+
await pg.afterEach();
88+
});
89+
90+
function actAs(user: { id: string; role: string }) {
91+
db.setContext({
92+
role: 'authenticated',
93+
'app.role': user.role,
94+
'app.user_id': user.id,
95+
});
96+
}
97+
98+
describe('prescriptions adversarial RLS', () => {
99+
// ───────────────────────────────────────────────────────────────────────
100+
// Toggle-driven test: only Alice should see Alice's prescriptions.
101+
// Flip `currentUser` at the top to watch RLS filter them out for others.
102+
// ───────────────────────────────────────────────────────────────────────
103+
it("currentUser can read Alice's two prescriptions", async () => {
104+
actAs(currentUser);
105+
const r = await db.query(
106+
`SELECT m.generic_name
107+
FROM prescriptions.prescriptions rx
108+
JOIN medications.medications m ON m.id = rx.medication_id
109+
WHERE rx.patient_id = $1
110+
ORDER BY m.generic_name`,
111+
[alice.id],
112+
);
113+
expect(r.rows.map((row: { generic_name: string }) => row.generic_name)).toEqual([
114+
'amoxicillin',
115+
'atorvastatin',
116+
]);
117+
});
118+
119+
// ───────────────────────────────────────────────────────────────────────
120+
// Mallory cannot write for someone else — WITH CHECK + clinician-only
121+
// write policy both reject the INSERT.
122+
// ───────────────────────────────────────────────────────────────────────
123+
it('Mallory cannot forge a prescription in Alice\'s name', async () => {
124+
actAs(mallory);
125+
const amox = await pg.query(
126+
`SELECT id FROM medications.medications WHERE generic_name = 'amoxicillin' LIMIT 1`,
127+
);
128+
const point = 'mallory_forge';
129+
await db.savepoint(point);
130+
await expect(
131+
db.query(
132+
`INSERT INTO prescriptions.prescriptions
133+
(patient_id, medication_id, dosage, route, frequency, quantity)
134+
VALUES ($1, $2, '500 mg', 'oral', 'PRN', 30)`,
135+
[alice.id, amox.rows[0].id],
136+
),
137+
).rejects.toThrow(/row-level security/i);
138+
await db.rollback(point);
139+
});
140+
141+
// ───────────────────────────────────────────────────────────────────────
142+
// Even with a known row ID, RLS returns zero rows to a non-owner.
143+
// This proves the USING policy isn't bypassable via guessed primary keys.
144+
// ───────────────────────────────────────────────────────────────────────
145+
it('Mallory sees zero rows even when querying Alice\'s row ID directly', async () => {
146+
const alice_rx = await pg.query(
147+
`SELECT id FROM prescriptions.prescriptions WHERE patient_id = $1 LIMIT 1`,
148+
[alice.id],
149+
);
150+
151+
actAs(mallory);
152+
const r = await db.query(
153+
`SELECT id FROM prescriptions.prescriptions WHERE id = $1`,
154+
[alice_rx.rows[0].id],
155+
);
156+
expect(r.rowCount).toBe(0);
157+
});
158+
});
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)