Skip to content

Commit 7012f5d

Browse files
Auth when reading deleted records (#820)
This addresses #819.
1 parent 0a043f2 commit 7012f5d

File tree

5 files changed

+113
-0
lines changed

5 files changed

+113
-0
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Our preferred code style has been codified into `eslint` rules. Feel free to tak
116116
| command | description |
117117
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
118118
| `npm run test:node` | runs tests and type checking |
119+
| `npm run test:node-grep` | runs specific tests matching a pattern. Requires the -g option. For example: `npm run test:node-grep -g "RecordsReadHandler.handle"` |
119120
| `npm run test:browser` | runs tests against browser bundles in headless browser |
120121
| `npm run test:browser-debug` | runs tests against browser bundles in debug-ready Chrome |
121122
| `npm run build` | transpiles `ts` -> `js` as `esm` and `cjs`, generates `esm` and `umd` bundles, and generates all type declarations |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"lint:fix": "eslint . --fix",
155155
"circular-dependency-check": "depcruise src",
156156
"test:node": "npm run compile-validators && tsc && c8 node --enable-source-maps node_modules/.bin/mocha \"dist/esm/tests/**/*.spec.js\"",
157+
"test:node-grep": "npm run compile-validators && tsc && c8 node --enable-source-maps node_modules/.bin/mocha \"dist/esm/tests/**/*.spec.js\" -- --grep $npm_config_grep",
157158
"test:browser": "npm run compile-validators && cross-env karma start karma.conf.cjs",
158159
"test:browser-debug": "npm run compile-validators && cross-env karma start karma.conf.debug.cjs",
159160
"license-check": "license-report --only=prod > license-report.json && node ./build/license-check.cjs",

src/core/dwn-error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,5 @@ export enum DwnErrorCode {
163163
UrlProtocolNotNormalized = 'UrlProtocolNotNormalized',
164164
UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable',
165165
UrlSchemaNotNormalized = 'UrlSchemaNotNormalized',
166+
RecordsReadInitialWriteNotFound = 'RecordsReadInitialWriteNotFound',
166167
};

src/handlers/records-read.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ export class RecordsReadHandler implements MethodHandler {
7171
if (matchedMessage.descriptor.method === DwnMethodName.Delete) {
7272
const recordsDeleteMessage = matchedMessage as RecordsDeleteMessage;
7373
const initialWrite = await RecordsWrite.fetchInitialRecordsWriteMessage(this.messageStore, tenant, recordsDeleteMessage.descriptor.recordId);
74+
75+
if (initialWrite === undefined) {
76+
return messageReplyFromError(new DwnError(
77+
DwnErrorCode.RecordsReadInitialWriteNotFound,
78+
'Initial write for deleted record not found'
79+
), 400);
80+
}
81+
82+
// Perform authorization before returning the delete and initial write messages
83+
const parsedInitialWrite = await RecordsWrite.parse(initialWrite);
84+
try {
85+
await RecordsReadHandler.authorizeRecordsRead(tenant, recordsRead, parsedInitialWrite, this.messageStore);
86+
} catch (error) {
87+
return messageReplyFromError(error, 401);
88+
}
89+
7490
return {
7591
status : { code: 404, detail: 'Not Found' },
7692
entry : {

tests/handlers/records-read.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,100 @@ export function testRecordsReadHandler(): void {
205205
expect(ArrayUtility.byteArraysEqual(dataFetched, dataBytes!)).to.be.true;
206206
});
207207

208+
it('should return 400 when fetching initial write for a deleted record fails', async () => {
209+
const alice = await TestDataGenerator.generateDidKeyPersona();
210+
211+
// Write a record
212+
const { message: writeMessage, dataStream } = await TestDataGenerator.generateRecordsWrite({ author: alice });
213+
const writeReply = await dwn.processMessage(alice.did, writeMessage, { dataStream });
214+
expect(writeReply.status.code).to.equal(202);
215+
216+
// Delete the record
217+
const recordsDelete = await RecordsDelete.create({
218+
signer : Jws.createSigner(alice),
219+
recordId : writeMessage.recordId
220+
});
221+
const deleteReply = await dwn.processMessage(alice.did, recordsDelete.message);
222+
expect(deleteReply.status.code).to.equal(202);
223+
224+
// Stub the messageStore.query method to simulate failure in fetching initial write
225+
const queryStub = sinon.stub(dwn['messageStore'], 'query');
226+
queryStub.onFirstCall().resolves({ messages: [recordsDelete.message] });
227+
queryStub.onSecondCall().resolves({ messages: [] }); // Simulate no initial write found
228+
229+
// Attempt to read the deleted record
230+
const recordsRead = await RecordsRead.create({
231+
filter : { recordId: writeMessage.recordId },
232+
signer : Jws.createSigner(alice)
233+
});
234+
const readReply = await dwn.processMessage(alice.did, recordsRead.message);
235+
236+
// Verify the response
237+
expect(readReply.status.code).to.equal(400);
238+
expect(readReply.status.detail).to.contain(DwnErrorCode.RecordsReadInitialWriteNotFound);
239+
240+
// Restore the original messageStore.query method
241+
queryStub.restore();
242+
});
243+
244+
it('should return 401 when a non-author attempts to read the initial write of a deleted record', async () => {
245+
const alice = await TestDataGenerator.generateDidKeyPersona();
246+
const bob = await TestDataGenerator.generateDidKeyPersona();
247+
const carol = await TestDataGenerator.generateDidKeyPersona();
248+
249+
// Alice installs a protocol that allows anyone to write
250+
const protocolDefinition: ProtocolDefinition = {
251+
published : true,
252+
protocol : 'https://example.com/foo',
253+
types : {
254+
foo: {}
255+
},
256+
structure: {
257+
foo: {
258+
$actions: [{
259+
who : 'anyone',
260+
can : ['create', 'delete']
261+
}]
262+
}
263+
}
264+
};
265+
266+
const configureProtocol = await TestDataGenerator.generateProtocolsConfigure({
267+
author : alice,
268+
protocolDefinition : protocolDefinition,
269+
});
270+
const configureProtocolReply = await dwn.processMessage(alice.did, configureProtocol.message);
271+
expect(configureProtocolReply.status.code).to.equal(202);
272+
273+
// Bob writes a record to Alice's DWN
274+
const { message: writeMessage, dataStream } = await TestDataGenerator.generateRecordsWrite({
275+
author : bob,
276+
protocol : protocolDefinition.protocol,
277+
protocolPath : 'foo'
278+
});
279+
const writeReply = await dwn.processMessage(alice.did, writeMessage, { dataStream });
280+
expect(writeReply.status.code).to.equal(202);
281+
282+
// Bob deletes the record
283+
const recordsDelete = await RecordsDelete.create({
284+
signer : Jws.createSigner(bob),
285+
recordId : writeMessage.recordId
286+
});
287+
const deleteReply = await dwn.processMessage(alice.did, recordsDelete.message);
288+
expect(deleteReply.status.code).to.equal(202);
289+
290+
// Carol attempts to read the deleted record
291+
const recordsRead = await RecordsRead.create({
292+
filter : { recordId: writeMessage.recordId },
293+
signer : Jws.createSigner(carol)
294+
});
295+
const readReply = await dwn.processMessage(alice.did, recordsRead.message);
296+
297+
// Verify the response
298+
expect(readReply.status.code).to.equal(401);
299+
expect(readReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);
300+
});
301+
208302
it('should allow a non-tenant to read RecordsRead data they have authored', async () => {
209303
const alice = await TestDataGenerator.generateDidKeyPersona();
210304
const bob = await TestDataGenerator.generateDidKeyPersona();

0 commit comments

Comments
 (0)