Skip to content

Commit

Permalink
Support protocol authorized RecordsDelete (#576)
Browse files Browse the repository at this point in the history
* Support protocol authorized RecordsDelete

* Flat space authz failure test

* Lint

* Flesh out tests

* Lint

* Turn error into DwnErrorCode
  • Loading branch information
diehuxx committed Oct 31, 2023
1 parent c0f2a95 commit 66ec588
Show file tree
Hide file tree
Showing 16 changed files with 540 additions and 35 deletions.
2 changes: 2 additions & 0 deletions json-schemas/interface-methods/protocol-rule-set.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"can": {
"type": "string",
"enum": [
"delete",
"read",
"update",
"write"
Expand All @@ -63,6 +64,7 @@
"can": {
"type": "string",
"enum": [
"delete",
"query",
"read",
"update",
Expand Down
2 changes: 2 additions & 0 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export enum DwnErrorCode {
ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized',
ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized',
RecordsDecryptNoMatchingKeyEncryptedFound = 'RecordsDecryptNoMatchingKeyEncryptedFound',
RecordsDeleteAuthorizationFailed = 'RecordsDeleteAuthorizationFailed',

RecordsGrantAuthorizationConditionPublicationProhibited = 'RecordsGrantAuthorizationConditionPublicationProhibited',
RecordsGrantAuthorizationConditionPublicationRequired = 'RecordsGrantAuthorizationConditionPublicationRequired',
RecordsGrantAuthorizationScopeContextIdMismatch = 'RecordsGrantAuthorizationScopeContextIdMismatch',
Expand Down
93 changes: 74 additions & 19 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Filter } from '../types/message-types.js';
import type { MessageStore } from '../types/message-store.js';
import type { RecordsDelete } from '../interfaces/records-delete.js';
import type { RecordsQuery } from '../interfaces/records-query.js';
import type { RecordsRead } from '../interfaces/records-read.js';
import type { RecordsWriteMessage } from '../types/records-types.js';
Expand Down Expand Up @@ -196,6 +197,51 @@ export class ProtocolAuthorization {
);
}

public static async authorizeDelete(
tenant: string,
incomingMessage: RecordsDelete,
newestRecordsWrite: RecordsWrite,
messageStore: MessageStore,
): Promise<void> {

// fetch ancestor message chain
const ancestorMessageChain: RecordsWriteMessage[] =
await ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, newestRecordsWrite, messageStore);

// fetch the protocol definition
const protocolDefinition = await ProtocolAuthorization.fetchProtocolDefinition(
tenant,
newestRecordsWrite.message.descriptor.protocol!,
messageStore,
);

// get the rule set for the inbound message
const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(
newestRecordsWrite.message.descriptor.protocolPath!,
protocolDefinition,
);

// If the incoming message has `protocolRole` in the descriptor, validate the invoked role
await ProtocolAuthorization.verifyInvokedRole(
tenant,
incomingMessage,
newestRecordsWrite.message.descriptor.protocol!,
newestRecordsWrite.message.contextId!,
protocolDefinition,
messageStore,
);

// verify method invoked against the allowed actions
await ProtocolAuthorization.verifyAllowedActions(
tenant,
incomingMessage,
inboundMessageRuleSet,
ancestorMessageChain,
messageStore,
);

}

/**
* Fetches the protocol definition based on the protocol specified in the given message.
*/
Expand Down Expand Up @@ -226,7 +272,7 @@ export class ProtocolAuthorization {
*/
private static async constructAncestorMessageChain(
tenant: string,
incomingMessage: RecordsRead | RecordsWrite,
incomingMessage: RecordsDelete | RecordsRead | RecordsWrite,
recordsWrite: RecordsWrite,
messageStore: MessageStore
)
Expand Down Expand Up @@ -375,7 +421,7 @@ export class ProtocolAuthorization {
*/
private static async verifyInvokedRole(
tenant: string,
incomingMessage: RecordsQuery | RecordsRead | RecordsWrite,
incomingMessage: RecordsDelete | RecordsQuery | RecordsRead | RecordsWrite,
protocolUri: string,
contextId: string | undefined,
protocolDefinition: ProtocolDefinition,
Expand Down Expand Up @@ -430,26 +476,35 @@ export class ProtocolAuthorization {
*/
private static async getActionsSeekingARuleMatch(
tenant: string,
incomingMessage: RecordsQuery | RecordsRead | RecordsWrite,
incomingMessage: RecordsDelete | RecordsQuery | RecordsRead | RecordsWrite,
messageStore: MessageStore,
): Promise<ProtocolAction[]> {
if (incomingMessage.message.descriptor.method === DwnMethodName.Read) {
return [ProtocolAction.Read];
} else if (incomingMessage.message.descriptor.method === DwnMethodName.Query) {

switch (incomingMessage.message.descriptor.method) {
case DwnMethodName.Delete:
return [ProtocolAction.Delete];

case DwnMethodName.Query:
return [ProtocolAction.Query];
}
// else 'Write'

const incomingRecordsWrite = incomingMessage as RecordsWrite;
if (await incomingRecordsWrite.isInitialWrite()) {
// only 'write' allows initial RecordsWrites; 'update' only applies to subsequent RecordsWrites
return [ProtocolAction.Write];
} else if (await incomingRecordsWrite.isAuthoredByInitialRecordAuthor(tenant, messageStore)) {
// Both 'update' and 'write' authorize the incoming message
return [ProtocolAction.Write, ProtocolAction.Update];
} else {
// Actors other than the initial record author must be authorized to 'update' the message
return [ProtocolAction.Update];
case DwnMethodName.Read:
return [ProtocolAction.Read];

case DwnMethodName.Write:
const incomingRecordsWrite = incomingMessage as RecordsWrite;
if (await incomingRecordsWrite.isInitialWrite()) {
// only 'write' allows initial RecordsWrites; 'update' only applies to subsequent RecordsWrites
return [ProtocolAction.Write];
} else if (await incomingRecordsWrite.isAuthoredByInitialRecordAuthor(tenant, messageStore)) {
// Both 'update' and 'write' authorize the incoming message
return [ProtocolAction.Write, ProtocolAction.Update];
} else {
// Actors other than the initial record author must be authorized to 'update' the message
return [ProtocolAction.Update];
}

// default:
// not reachable in typescript
}
}

Expand All @@ -459,7 +514,7 @@ export class ProtocolAuthorization {
*/
private static async verifyAllowedActions(
tenant: string,
incomingMessage: RecordsQuery | RecordsRead | RecordsWrite,
incomingMessage: RecordsDelete | RecordsQuery | RecordsRead | RecordsWrite,
inboundMessageRuleSet: ProtocolRuleSet,
ancestorMessageChain: RecordsWriteMessage[],
messageStore: MessageStore,
Expand Down
17 changes: 14 additions & 3 deletions src/handlers/records-delete.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { EventLog } from '../types/event-log.js';
import type { GenericMessageReply } from '../core/message-reply.js';
import type { MethodHandler } from '../types/method-handler.js';
import type { RecordsDeleteMessage } from '../types/records-types.js';
import type { DataStore, DidResolver, MessageStore } from '../index.js';
import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';

import { authenticate } from '../core/auth.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { RecordsDelete } from '../interfaces/records-delete.js';
import { RecordsWrite } from '../index.js';
import { StorageController } from '../store/storage-controller.js';
import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js';

Expand All @@ -26,10 +27,9 @@ export class RecordsDeleteHandler implements MethodHandler {
return messageReplyFromError(e, 400);
}

// authentication & authorization
// authentication
try {
await authenticate(message.authorization, this.didResolver);
await recordsDelete.authorize(tenant);
} catch (e) {
return messageReplyFromError(e, 401);
}
Expand Down Expand Up @@ -66,6 +66,17 @@ export class RecordsDeleteHandler implements MethodHandler {
};
}

// authorization
try {
await recordsDelete.authorize(
tenant,
await RecordsWrite.parse(newestExistingMessage as RecordsWriteMessage),
this.messageStore
);
} catch (e) {
return messageReplyFromError(e, 401);
}

const indexes = await constructIndexes(tenant, recordsDelete);
await this.messageStore.put(tenant, message, indexes);

Expand Down
29 changes: 23 additions & 6 deletions src/interfaces/records-delete.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { MessageStore } from '../index.js';
import type { RecordsWrite } from './records-write.js';
import type { Signer } from '../types/signer.js';
import type { RecordsDeleteDescriptor, RecordsDeleteMessage } from '../types/records-types.js';

import { Message } from '../core/message.js';
import type { Signer } from '../types/signer.js';

import { authorize, validateMessageSignatureIntegrity } from '../core/auth.js';
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
import { validateMessageSignatureIntegrity } from '../core/auth.js';
import { DwnError, DwnErrorCode } from '../index.js';
import { DwnInterfaceName, DwnMethodName } from '../core/message.js';
import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js';

export type RecordsDeleteOptions = {
recordId: string;
messageTimestamp?: string;
protocolRole?: string;
authorizationSigner: Signer;
};

Expand Down Expand Up @@ -39,16 +44,28 @@ export class RecordsDelete extends Message<RecordsDeleteMessage> {
messageTimestamp : options.messageTimestamp ?? currentTime
};

const authorization = await Message.createAuthorizationAsAuthor(descriptor, options.authorizationSigner);
const authorization = await Message.createAuthorizationAsAuthor(
descriptor,
options.authorizationSigner,
{ protocolRole: options.protocolRole },
);
const message: RecordsDeleteMessage = { descriptor, authorization };

Message.validateJsonSchema(message);

return new RecordsDelete(message);
}

public async authorize(tenant: string): Promise<void> {
// TODO: #203 - implement protocol-based authorization for RecordsDelete (https://github.com/TBD54566975/dwn-sdk-js/issues/203)
await authorize(tenant, this);
public async authorize(tenant: string, newestRecordsWrite: RecordsWrite, messageStore: MessageStore): Promise<void> {
if (this.author === tenant) {
return;
} else if (newestRecordsWrite.message.descriptor.protocol !== undefined) {
await ProtocolAuthorization.authorizeDelete(tenant, this, newestRecordsWrite, messageStore);
} else {
throw new DwnError(
DwnErrorCode.RecordsDeleteAuthorizationFailed,
'RecordsDelete message failed authorization'
);
}
}
}
1 change: 1 addition & 0 deletions src/types/protocols-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum ProtocolActor {
}

export enum ProtocolAction {
Delete = 'delete',
Query = 'query',
Read = 'read',
Update = 'update',
Expand Down

0 comments on commit 66ec588

Please sign in to comment.