Skip to content

Commit

Permalink
messages get authorization
Browse files Browse the repository at this point in the history
  • Loading branch information
LiranCohen committed Jun 25, 2024
1 parent 0588ee7 commit afa5590
Show file tree
Hide file tree
Showing 11 changed files with 748 additions and 128 deletions.
3 changes: 3 additions & 0 deletions json-schemas/permissions/permissions-definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/events-query-scope"
},
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-get-scope"
},
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-query-scope"
},
Expand Down
19 changes: 19 additions & 0 deletions json-schemas/permissions/scopes.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@
}
}
},
"messages-get-scope": {
"type": "object",
"additionalProperties": false,
"required": [
"interface",
"method"
],
"properties": {
"interface": {
"const": "Messages"
},
"method": {
"const": "Get"
},
"protocol": {
"type": "string"
}
}
},
"protocols-query-scope": {
"type": "object",
"additionalProperties": false,
Expand Down
6 changes: 5 additions & 1 deletion src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ export enum DwnErrorCode {
IndexInvalidSortPropertyInMemory = 'IndexInvalidSortPropertyInMemory',
IndexMissingIndexableProperty = 'IndexMissingIndexableProperty',
JwsDecodePlainObjectPayloadInvalid = 'JwsDecodePlainObjectPayloadInvalid',
MessageGetInvalidCid = 'MessageGetInvalidCid',
MessagesGetInvalidCid = 'MessagesGetInvalidCid',
MessagesGetWriteRecordNotFound = 'MessagesGetWriteRecordNotFound',
MessagesGetAuthorizationFailed = 'MessagesGetAuthorizationFailed',
MessagesGetVerifyScopeFailed = 'MessagesGetVerifyScopeFailed',
ParseCidCodecNotSupported = 'ParseCidCodecNotSupported',
ParseCidMultihashNotSupported = 'ParseCidMultihashNotSupported',
PermissionsProtocolCreateGrantRecordsScopeMissingProtocol = 'PermissionsProtocolCreateGrantRecordsScopeMissingProtocol',
PermissionsProtocolCreateRequestRecordsScopeMissingProtocol = 'PermissionsProtocolCreateRequestRecordsScopeMissingProtocol',
PermissionsProtocolInvalidProtocolURI = 'PermissionsProtocolInvalidProtocolURI',
PermissionsProtocolValidateSchemaUnexpectedRecord = 'PermissionsProtocolValidateSchemaUnexpectedRecord',
PermissionsProtocolValidateScopeContextIdProhibitedProperties = 'PermissionsProtocolValidateScopeContextIdProhibitedProperties',
PermissionsProtocolValidateScopeProtocolMismatch = 'PermissionsProtocolValidateScopeProtocolMismatch',
Expand Down
94 changes: 94 additions & 0 deletions src/core/messages-grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { GenericMessage } from '../types/message-types.js';
import type { MessagesGetMessage } from '../types/messages-types.js';
import type { MessagesPermissionScope } from '../types/permission-types.js';
import type { MessageStore } from '../types/message-store.js';
import type { PermissionGrant } from '../protocols/permission-grant.js';
import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';

import { GrantAuthorization } from './grant-authorization.js';
import { Message } from './message.js';
import { Records } from '../utils/records.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 MessagesPermissionScope;
await MessagesGrantAuthorization.verifyScope(expectedGrantor, messageToGet, scope, messageStore);
}

/**
* Verifies the given record against the scope of the given grant.
*/
private static async verifyScope(
tenant: string,
messageToGet: GenericMessage,
incomingScope: MessagesPermissionScope,
messageStore: MessageStore,
): Promise<void> {
if (incomingScope.protocol === undefined) {
// if no protocol is specified in the scope, then the grant is for all records
return;
}

if (messageToGet.descriptor.interface === DwnInterfaceName.Records) {
const recordsMessage = messageToGet as RecordsWriteMessage | RecordsDeleteMessage;
const recordsWriteMessage = Records.isRecordsWrite(recordsMessage) ? recordsMessage :
await MessagesGrantAuthorization.getRecordsWriteMessageToAuthorize(tenant, recordsMessage, messageStore);

if (recordsWriteMessage.descriptor.protocol === incomingScope.protocol) {
// the record protocol matches the incoming scope protocol
return;
}
}

throw new DwnError(DwnErrorCode.MessagesGetVerifyScopeFailed, 'record message failed scope authorization');
}

private static async getRecordsWriteMessageToAuthorize(
tenant: string,
message: RecordsDeleteMessage,
messageStore: MessageStore
): Promise<RecordsWriteMessage> {
// get existing RecordsWrite messages matching the `recordId`
const query = {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
recordId : message.descriptor.recordId
};

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

// It shouldn't be possible to get here, as the `RecordsDeleteMessage` should always have a corresponding `RecordsWriteMessage`.
// But we add this in for defensive programming
throw new DwnError(DwnErrorCode.MessagesGetWriteRecordNotFound, 'record not found');
}
}
53 changes: 46 additions & 7 deletions src/handlers/messages-get.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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 { RecordsQueryReplyEntry } 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 { Encoder } from '../utils/encoder.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,35 +32,41 @@ export class MessagesGetHandler implements MethodHandler {

try {
await authenticate(message.authorization, this.didResolver);
await authorizeOwner(tenant, messagesGet);
} catch (e) {
return messageReplyFromError(e, 401);
}

const messageResult = await this.messageStore.get(tenant, message.descriptor.messageCid);

if (messageResult === undefined) {
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
const entry: MessagesGetReplyEntry = { message: messageResult, messageCid: message.descriptor.messageCid };
if (entry.message && Records.isRecordsWrite(messageResult)) {
const recordsWrite = entry.message as DataEncodedRecordsWriteMessage;
if (Records.isRecordsWrite(messageResult)) {
const recordsWrite = entry.message as RecordsQueryReplyEntry;
// RecordsWrite specific handling, if MessageStore has embedded `encodedData` return it with the entry.
// we store `encodedData` along with the message if the data is below a certain threshold.
if (recordsWrite.encodedData !== undefined) {
const dataBytes = Encoder.base64UrlToBytes(recordsWrite.encodedData);
entry.message.data = DataStream.fromBytes(dataBytes);
delete recordsWrite.encodedData;
} else {
// check the data store for the associated data
const result = await this.dataStore.get(tenant, recordsWrite.recordId, recordsWrite.descriptor.dataCid);
if (result?.dataStream !== undefined) {
entry.message.data = result.dataStream;
} else {
delete entry.message.data; // if there is no data, return with the data property undefined
// if there is no data, return with the data property undefined
delete entry.message.data;
}
}
}
Expand All @@ -66,4 +76,33 @@ 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 (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 {
throw new DwnError(DwnErrorCode.MessagesGetAuthorizationFailed, 'protocol message failed authorization');
}
}
}
12 changes: 9 additions & 3 deletions src/interfaces/messages-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type MessagesGetOptions = {
messageCid: string;
signer: Signer;
messageTimestamp?: string;
permissionGrantId?: string;
};

export class MessagesGet extends AbstractMessage<MessagesGetMessage> {
Expand All @@ -30,10 +31,15 @@ export class MessagesGet extends AbstractMessage<MessagesGetMessage> {
interface : DwnInterfaceName.Messages,
method : DwnMethodName.Get,
messageCid : options.messageCid,
messageTimestamp : options?.messageTimestamp ?? Time.getCurrentTimestamp(),
messageTimestamp : options.messageTimestamp ?? Time.getCurrentTimestamp(),
};

const authorization = await Message.createAuthorization({ descriptor, signer: options.signer });
const { signer, permissionGrantId } = options;
const authorization = await Message.createAuthorization({
descriptor,
signer,
permissionGrantId,
});
const message = { descriptor, authorization };

Message.validateJsonSchema(message);
Expand All @@ -51,7 +57,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`);
}
}
}
3 changes: 1 addition & 2 deletions src/types/messages-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export type MessagesGetMessage = GenericMessage & {

export type MessagesGetReplyEntry = {
messageCid: string;
message?: (GenericMessage & { data?: Readable });
error?: string;
message: (GenericMessage & { data?: Readable });
};

export type MessagesGetReply = GenericMessageReply & {
Expand Down
1 change: 1 addition & 0 deletions src/types/permission-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type ProtocolPermissionScope = {
export type MessagesPermissionScope = {
interface: DwnInterfaceName.Messages;
method: DwnMethodName.Get;
protocol?: string;
};

export type EventsPermissionScope = {
Expand Down
Loading

0 comments on commit afa5590

Please sign in to comment.