Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support protocol authorized RecordsDelete #576

Merged
merged 6 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { getCurrentTimeInHighPrecision } from '../utils/time.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';

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

Expand Down Expand Up @@ -38,16 +43,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
Loading