Skip to content

Commit

Permalink
messageget grant authorizations
Browse files Browse the repository at this point in the history
  • Loading branch information
LiranCohen committed May 31, 2024
1 parent b225850 commit ab3ebf2
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 40 deletions.
7 changes: 6 additions & 1 deletion src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ export enum DwnErrorCode {
IndexInvalidSortPropertyInMemory = 'IndexInvalidSortPropertyInMemory',
IndexMissingIndexableProperty = 'IndexMissingIndexableProperty',
JwsDecodePlainObjectPayloadInvalid = 'JwsDecodePlainObjectPayloadInvalid',
MessageGetInvalidCid = 'MessageGetInvalidCid',
MessagesGetInvalidCid = 'MessagesGetInvalidCid',
MessagesGetAuthorizationFailed ='MessagesGetAuthorizationFailed',
MessagesGetGrantNotFound = 'MessagesGetGrantNotFound',
MessagesGetWriteRecordNotFound = 'MessagesGetWriteRecordNotFound',
MessagesGetRecordsAuthorizationFailed ='MessagesGetRecordsAuthorizationFailed',
MessagesGetGrantProtocolScopeAuthorizationFailed ='MessagesGetGrantProtocolScopeAuthorizationFailed',
ParseCidCodecNotSupported = 'ParseCidCodecNotSupported',
ParseCidMultihashNotSupported = 'ParseCidMultihashNotSupported',
PermissionsProtocolValidateSchemaUnexpectedRecord = 'PermissionsProtocolValidateSchemaUnexpectedRecord',
Expand Down
146 changes: 146 additions & 0 deletions src/core/messages-grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { GenericMessage } from '../types/message-types.js';
import type { MessagesGet } from '../interfaces/messages-get.js';
import type { MessagesGetMessage } from '../types/messages-types.js';
import type { MessageStore } from '../types/message-store.js';
import type { MessagesGetPermissionScope, RecordsPermissionScope } from '../types/permission-types.js';
import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';

import { GrantAuthorization } from './grant-authorization.js';
import { Message } from './message.js';
import { PermissionGrant } from '../protocols/permission-grant.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';

export class MessagesGrantAuthorization {

/**
* Authorizes a RecordsReadMessage using the given permission grant.
* @param messageStore Used to check if the given grant has been revoked.
*/
public static async authorizeMessagesGetGrant(input: {
messagesGetMessage: MessagesGetMessage,
messageToGet: GenericMessage,
expectedGrantor: string,
expectedGrantee: string,
permissionGrant: PermissionGrant,
messageStore: MessageStore,
}): Promise<void> {
const {
messagesGetMessage, messageToGet, expectedGrantor, expectedGrantee, permissionGrant, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: messagesGetMessage,
expectedGrantor,
expectedGrantee,
permissionGrant,
messageStore
});

const scope = permissionGrant.scope as MessagesGetPermissionScope;
await MessagesGrantAuthorization.verifyScope(expectedGrantor, messageToGet, scope, messageStore);
}

public static async authorizeMessagesGetRecords(input: {
tenant: string,
incomingMessage: MessagesGet,
recordMessage: RecordsWriteMessage | RecordsDeleteMessage,
messageStore: MessageStore,
}): Promise<void> {
const { tenant, incomingMessage, recordMessage, messageStore } = input;
const recordsWriteToAuthorize = await this.getRecordsWriteMessageToAuthorize(tenant, recordMessage, messageStore);

const { descriptor } = recordsWriteToAuthorize;
if (descriptor.published === true) {
// authentication is not required for published data
return;
} else if (incomingMessage.author !== undefined && incomingMessage.author === descriptor.recipient) {
// The recipient of a message may always read it
return;
} else {
// if we reached here, the record is not authorized
throw new DwnError(DwnErrorCode.MessagesGetRecordsAuthorizationFailed, 'record message failed authorization');
}
}

/**
* Verifies the given record against the scope of the given grant.
*/
private static async verifyScope(
tenant: string,
message: GenericMessage,
incomingScope: MessagesGetPermissionScope,
messageStore: MessageStore,
): Promise<void> {
if (incomingScope.protocol === undefined) {
// no protocol to verify, the grant has access to all records
return;
}

// Only protocol scopes are currently supported for Messages interface authorizations
if (message.descriptor.interface === DwnInterfaceName.Records) {
const recordMessage = message as RecordsWriteMessage | RecordsDeleteMessage;
const recordsWriteToAuthorize = await this.getRecordsWriteMessageToAuthorize(tenant, recordMessage, messageStore);
const { protocol: recordProtocol } = recordsWriteToAuthorize.descriptor;
if (recordProtocol === PermissionsProtocol.uri) {
// the incoming message is a grant or revocation message
// we need to get the protocol associated with the underlying grant

let grantMessage: RecordsWriteMessage | undefined;
if (recordsWriteToAuthorize.descriptor.protocolPath === PermissionsProtocol.revocationPath) {
// fetch the grant associated with the revocation
const permissionGrantId = recordsWriteToAuthorize.descriptor.parentId!;
const grantAuthorizedMessagesQuery = {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
protocol : PermissionsProtocol.uri,
protocolPath : PermissionsProtocol.grantPath,
recordId : permissionGrantId,
};
const { messages: grantMessages } = await messageStore.query(tenant, [ grantAuthorizedMessagesQuery ]);
grantMessage = await Message.getNewestMessage(grantMessages) as RecordsWriteMessage | undefined;
} else {
grantMessage = recordsWriteToAuthorize;
}

const grantMessageScope = grantMessage ? (await PermissionGrant.parse(grantMessage)).scope as RecordsPermissionScope : undefined;
if (grantMessageScope && grantMessageScope.protocol === incomingScope.protocol) {
// the record grant message has the same scope protocol as the incoming scope protocol
return;
}
} else {recordProtocol === incomingScope.protocol;} {
// the record protocol matches the incoming scope protocol
return;
}
}

throw new DwnError(DwnErrorCode.MessagesGetGrantProtocolScopeAuthorizationFailed, 'record message failed protocol authorization');
}

private static async getRecordsWriteMessageToAuthorize(
tenant: string,
message: RecordsWriteMessage | RecordsDeleteMessage,
messageStore: MessageStore
): Promise<RecordsWriteMessage> {
if (message.descriptor.method === DwnMethodName.Write) {
return message as RecordsWriteMessage;
} else {
// it is a RecordsDeleteMessage, need to fetch the latest RecordWrite to verify the authorization
// get existing RecordsWrite messages matching the `recordId`
const query = {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
recordId : (message as RecordsDeleteMessage).descriptor.recordId
};

const { messages: existingMessages } = await messageStore.query(tenant, [ query ]);
const newestWrite = await Message.getNewestMessage(existingMessages);
if (newestWrite !== undefined) {
return newestWrite as RecordsWriteMessage;
}
}

throw new DwnError(DwnErrorCode.MessagesGetWriteRecordNotFound, 'record not found');
}
}
57 changes: 54 additions & 3 deletions src/handlers/messages-get.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js';
import type { DataStore } from '../types/data-store.js';
import type { DidResolver } from '@web5/dids';
import type { GenericMessage } from '../types/message-types.js';
import type { MessageStore } from '../types/message-store.js';
import type { MethodHandler } from '../types/method-handler.js';
import type { DataEncodedRecordsWriteMessage, RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';
import type { MessagesGetMessage, MessagesGetReply, MessagesGetReplyEntry } from '../types/messages-types.js';

import { authenticate } from '../core/auth.js';
import { DataStream } from '../utils/data-stream.js';
import { DwnInterfaceName } from '../enums/dwn-interface-method.js';
import { Encoder } from '../utils/encoder.js';
import { Message } from '../core/message.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { MessagesGet } from '../interfaces/messages-get.js';
import { MessagesGrantAuthorization } from '../core/messages-grant-authorization.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { Records } from '../utils/records.js';
import { authenticate, authorizeOwner } from '../core/auth.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';

type HandleArgs = { tenant: string, message: MessagesGetMessage };

Expand All @@ -28,7 +34,6 @@ export class MessagesGetHandler implements MethodHandler {

try {
await authenticate(message.authorization, this.didResolver);
await authorizeOwner(tenant, messagesGet);
} catch (e) {
return messageReplyFromError(e, 401);
}
Expand All @@ -39,6 +44,12 @@ export class MessagesGetHandler implements MethodHandler {
return { status: { code: 404, detail: 'Not Found' } };
}

try {
await MessagesGetHandler.authorizeMessagesGet(tenant, messagesGet, messageResult, this.messageStore);
} catch (error) {
return messageReplyFromError(error, 401);
}

// Include associated data as `encodedData` IF:
// * its a RecordsWrite
// * `encodedData` exists which means the data size is equal or smaller than the size threshold
Expand Down Expand Up @@ -66,4 +77,44 @@ export class MessagesGetHandler implements MethodHandler {
entry
};
}

/**
* @param messageStore Used to check if the grant has been revoked.
*/
private static async authorizeMessagesGet(
tenant: string,
messagesGet: MessagesGet,
matchedMessage: GenericMessage,
messageStore: MessageStore
): Promise<void> {
if (Message.isSignedByAuthorDelegate(matchedMessage)) {
throw new Error('not implemented.');
}

if (messagesGet.author === tenant) {
// If the author is the tenant, no further authorization is needed
return;
} if (messagesGet.author !== undefined && messagesGet.signaturePayload!.permissionGrantId !== undefined) {
// if the author is not the tenant and the message has a permissionGrantId, we need to authorize the grant
const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, messagesGet.signaturePayload!.permissionGrantId);
await MessagesGrantAuthorization.authorizeMessagesGetGrant({
messagesGetMessage : messagesGet.message,
messageToGet : matchedMessage,
expectedGrantor : tenant,
expectedGrantee : messagesGet.author,
permissionGrant,
messageStore
});
} else if (matchedMessage.descriptor.interface === DwnInterfaceName.Records) {
// If the message is not a grant and the interface is Records, we need to authorize the Records message
await MessagesGrantAuthorization.authorizeMessagesGetRecords({
tenant,
incomingMessage : messagesGet,
recordMessage : matchedMessage as RecordsWriteMessage | RecordsDeleteMessage,
messageStore
});
} else {
throw new DwnError(DwnErrorCode.MessagesGetAuthorizationFailed, 'message failed authorization');
}
}
}
2 changes: 1 addition & 1 deletion src/interfaces/messages-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class MessagesGet extends AbstractMessage<MessagesGetMessage> {
try {
Cid.parseCid(messageCid);
} catch (_) {
throw new DwnError(DwnErrorCode.MessageGetInvalidCid, `${messageCid} is not a valid CID`);
throw new DwnError(DwnErrorCode.MessagesGetInvalidCid, `${messageCid} is not a valid CID`);
}
}
}
4 changes: 4 additions & 0 deletions src/types/permission-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export type RecordsPermissionScope = {
schema?: string;
};

export type MessagesGetPermissionScope = {
protocol?: string;
};

export enum PermissionConditionPublication {
Required = 'Required',
Prohibited = 'Prohibited',
Expand Down
54 changes: 19 additions & 35 deletions tests/handlers/messages-get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,18 @@ export function testMessagesGetHandler(): void {
it('returns a 401 if tenant is not author', async () => {
const alice = await TestDataGenerator.generateDidKeyPersona();
const bob = await TestDataGenerator.generateDidKeyPersona();
const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ author: alice });

// bob creates a record and writes it to his DWN
const { message: recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ author: bob });
const { status } = await dwn.processMessage(bob.did, recordsWrite, { dataStream });
expect(status.code).to.equal(202);

const { message } = await TestDataGenerator.generateMessagesGet({
author : alice,
messageCid : await Message.getCid(recordsWrite.message)
messageCid : await Message.getCid(recordsWrite)
});

// alice is not the author of the message
const reply = await dwn.processMessage(bob.did, message);

expect(reply.status.code).to.equal(401);
Expand Down Expand Up @@ -117,19 +122,22 @@ export function testMessagesGetHandler(): void {
expect(reply.status.code).to.equal(404);
});

it('returns a data stream if the data is larger than the encodedData threshold', async () => {
});

describe('gets data in the reply entry', () => {
it('data is less than threshold', async () => {
const alice = await TestDataGenerator.generateDidKeyPersona();

const { recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({
const { message: recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({
author : alice,
data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded),
});

const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), { dataStream });
const reply = await dwn.processMessage(alice.did, recordsWrite, { dataStream });
expect(reply.status.code).to.equal(202);

const recordsWriteMessageCid = await Message.getCid(recordsWrite.message);
const recordsWriteMessageCid = await Message.getCid(recordsWrite);
const { message } = await TestDataGenerator.generateMessagesGet({
author : alice,
messageCid : recordsWriteMessageCid
Expand All @@ -152,15 +160,15 @@ export function testMessagesGetHandler(): void {
it('data is greater than threshold', async () => {
const alice = await TestDataGenerator.generateDidKeyPersona();

const { recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({
const { message: recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({
author : alice,
data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 10),
});

const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), { dataStream });
const reply = await dwn.processMessage(alice.did, recordsWrite, { dataStream });
expect(reply.status.code).to.equal(202);

const recordsWriteMessageCid = await Message.getCid(recordsWrite.message);
const recordsWriteMessageCid = await Message.getCid(recordsWrite);
const { message } = await TestDataGenerator.generateMessagesGet({
author : alice,
messageCid : recordsWriteMessageCid
Expand All @@ -184,14 +192,14 @@ export function testMessagesGetHandler(): void {
const alice = await TestDataGenerator.generateDidKeyPersona();

// initial write
const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({
const { message: recordsWriteMessage, recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({
author : alice,
data : TestDataGenerator.randomBytes(DwnConstant.maxDataSizeAllowedToBeEncoded + 10),
});

const initialMessageCid = await Message.getCid(recordsWrite.message);
const initialMessageCid = await Message.getCid(recordsWriteMessage);

let reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), { dataStream });
let reply = await dwn.processMessage(alice.did, recordsWriteMessage, { dataStream });
expect(reply.status.code).to.equal(202);

const { recordsWrite: updateMessage, dataStream: updateDataStream } = await TestDataGenerator.generateFromRecordsWrite({
Expand Down Expand Up @@ -220,29 +228,5 @@ export function testMessagesGetHandler(): void {
expect(messageReply.message?.data).to.be.undefined;
});
});

it('returns a data stream if the data is larger than the encodedData threshold', async () => {
});

it('does not return messages that belong to other tenants', async () => {
const alice = await TestDataGenerator.generateDidKeyPersona();
const bob = await TestDataGenerator.generateDidKeyPersona();

const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({
author: alice
});

const reply = await dwn.processMessage(alice.did, recordsWrite.toJSON(), { dataStream });
expect(reply.status.code).to.equal(202);

const { message } = await TestDataGenerator.generateMessagesGet({
author : bob,
messageCid : await Message.getCid(recordsWrite.message)
});

// returns a 404 because the RecordsWrite created above is not bob's
const messagesGetReply: MessagesGetReply = await dwn.processMessage(bob.did, message);
expect(messagesGetReply.status.code).to.equal(404);
});
});
}

0 comments on commit ab3ebf2

Please sign in to comment.