Skip to content

Commit

Permalink
#564 - Added delegated grant support for RecordsRead (#605)
Browse files Browse the repository at this point in the history
* #564 - Added delegated grant support for RecordsRead
* Update names with Message, AbstractMessage, and MessageInterface
  • Loading branch information
thehenrytsai committed Nov 14, 2023
1 parent 5740a26 commit 947a372
Show file tree
Hide file tree
Showing 24 changed files with 166 additions and 116 deletions.
6 changes: 6 additions & 0 deletions Q_AND_A.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,10 @@

This design choice is primarily driven by performance considerations. If we were to make `protocolPath` optional, and it is not specified, we would need to search records across protocol paths. Since protocol rules (protocol rule set) are defined at the per protocol path level, this means we would need to parse the protocol rules for every protocol path in the protocol definition to determine which protocol path the invoked role has access to. Then, we would need to make a database query for each qualified protocol path, which could be quite costly. This is not to say that we should never consider it, but this is the current design choice.

- What is the difference between `write` and `update` actions?

(Last update: 2023/11/09)

- `write` - allows a DID to create and update the record they have created
- `update` - allows a DID to update a record, regardless of the initial author

2 changes: 1 addition & 1 deletion json-schemas/interface-methods/records-read.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
],
"properties": {
"authorization": {
"$ref": "https://identity.foundation/dwn/json-schemas/authorization.json"
"$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json"
},
"descriptor": {
"type": "object",
Expand Down
48 changes: 48 additions & 0 deletions src/core/abstract-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { MessageInterface } from '../types/message-interface.js';
import type { GenericMessage, GenericSignaturePayload } from '../types/message-types.js';

import { Jws } from '../utils/jws.js';
import { Message } from './message.js';

/**
* An abstract implementation of the `MessageInterface` interface.
*/
export abstract class AbstractMessage<M extends GenericMessage> implements MessageInterface<M> {
private _message: M;
public get message(): M {
return this._message as M;
}

private _author: string | undefined;
public get author(): string | undefined {
return this._author;
}

private _signaturePayload: GenericSignaturePayload | undefined;
public get signaturePayload(): GenericSignaturePayload | undefined {
return this._signaturePayload;
}

protected constructor(message: M) {
this._message = message;

if (message.authorization !== undefined) {
// if the message authorization contains author delegated grant, the author would be the grantor of the grant
// else the author would be the signer of the message
if (message.authorization.authorDelegatedGrant !== undefined) {
this._author = Message.getSigner(message.authorization.authorDelegatedGrant);
} else {
this._author = Message.getSigner(message as GenericMessage);
}

this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
}
}

/**
* Called by `JSON.stringify(...)` automatically.
*/
toJSON(): GenericMessage {
return this.message;
}
}
1 change: 1 addition & 0 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export enum DwnErrorCode {
PrivateKeySignerUnableToDeduceKeyId = 'PrivateKeySignerUnableToDeduceKeyId',
PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve',
ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed',
ProtocolAuthorizationActionRulesNotFound = 'ProtocolAuthorizationActionRulesNotFound',
ProtocolAuthorizationDuplicateContextRoleRecipient = 'ProtocolAuthorizationDuplicateContextRoleRecipient',
ProtocolAuthorizationDuplicateGlobalRoleRecipient = 'ProtocolAuthorizationDuplicateGlobalRoleRecipient',
ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat',
Expand Down
3 changes: 2 additions & 1 deletion src/core/grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { GenericMessage } from '../types/message-types.js';
import type { MessageInterface } from '../types/message-interface.js';
import type { MessageStore } from '../types/message-store.js';
import type { PermissionsGrantMessage } from '../types/permissions-types.js';

Expand All @@ -15,7 +16,7 @@ export class GrantAuthorization {
*/
public static async authorizeGenericMessage(
tenant: string,
incomingMessage: Message<GenericMessage>,
incomingMessage: MessageInterface<GenericMessage>,
author: string,
permissionsGrantId: string,
messageStore: MessageStore,
Expand Down
25 changes: 4 additions & 21 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,10 @@ import { removeUndefinedProperties } from '../utils/object.js';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';

export abstract class Message<M extends GenericMessage> {
readonly message: M;
readonly signaturePayload: GenericSignaturePayload | undefined;
readonly author: string | undefined;

constructor(message: M) {
this.message = message;

if (message.authorization !== undefined) {
this.signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
this.author = Message.getSigner(message as GenericMessage);
}
}

/**
* Called by `JSON.stringify(...)` automatically.
*/
toJSON(): GenericMessage {
return this.message;
}

/**
* A class containing utility methods for working with DWN messages.
*/
export class Message {
/**
* Validates the given message against the corresponding JSON schema.
* @throws {Error} if fails validation.
Expand Down
5 changes: 4 additions & 1 deletion src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ export class ProtocolAuthorization {

/**
* Returns a list of ProtocolAction(s) based on the incoming message, one of which must be allowed for the message to be authorized.
* NOTE: the reason why there could be multiple actions is because in case of an "update" RecordsWrite by the original record author,
* the RecordsWrite can either be authorized by a `write` or `update` allow rule. It is important to recognize that the `write` access that allowed
* the original record author to create the record maybe revoked (e.g. by role revocation) by the time an "update" by the same author is attempted.
*/
private static async getActionsSeekingARuleMatch(
tenant: string,
Expand Down Expand Up @@ -529,7 +532,7 @@ export class ProtocolAuthorization {
// We have already checked that the message is not from tenant, owner, or permissionsGrant
if (actionRules === undefined) {
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationActionNotAllowed,
DwnErrorCode.ProtocolAuthorizationActionRulesNotFound,
`no action rule defined for ${incomingMessageMethod}, ${author} is unauthorized`
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/events-get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Signer } from '../types/signer.js';
import type { EventsGetDescriptor, EventsGetMessage } from '../types/event-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { Time } from '../utils/time.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
Expand All @@ -11,7 +12,7 @@ export type EventsGetOptions = {
messageTimestamp?: string;
};

export class EventsGet extends Message<EventsGetMessage> {
export class EventsGet extends AbstractMessage<EventsGetMessage> {

public static async parse(message: EventsGetMessage): Promise<EventsGet> {
Message.validateJsonSchema(message);
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/messages-get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Signer } from '../types/signer.js';
import type { MessagesGetDescriptor, MessagesGetMessage } from '../types/messages-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Cid } from '../utils/cid.js';
import { Message } from '../core/message.js';
import { Time } from '../utils/time.js';
Expand All @@ -13,7 +14,7 @@ export type MessagesGetOptions = {
messageTimestamp?: string;
};

export class MessagesGet extends Message<MessagesGetMessage> {
export class MessagesGet extends AbstractMessage<MessagesGetMessage> {
public static async parse(message: MessagesGetMessage): Promise<MessagesGet> {
Message.validateJsonSchema(message);
this.validateMessageCids(message.descriptor.messageCids);
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/permissions-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PermissionsRequest } from './permissions-request.js';
import type { Signer } from '../types/signer.js';
import type { PermissionConditions, PermissionScope, PermissionsGrantDescriptor, RecordsPermissionScope } from '../types/permissions-grant-descriptor.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { removeUndefinedProperties } from '../utils/object.js';
import { Time } from '../utils/time.js';
Expand Down Expand Up @@ -35,7 +36,7 @@ export type CreateFromPermissionsRequestOverrides = {
conditions?: PermissionConditions;
};

export class PermissionsGrant extends Message<PermissionsGrantMessage> {
export class PermissionsGrant extends AbstractMessage<PermissionsGrantMessage> {

public static async parse(message: PermissionsGrantMessage): Promise<PermissionsGrant> {
await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor);
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/permissions-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Signer } from '../types/signer.js';
import type { PermissionConditions, PermissionScope } from '../types/permissions-grant-descriptor.js';
import type { PermissionsRequestDescriptor, PermissionsRequestMessage } from '../types/permissions-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { removeUndefinedProperties } from '../utils/object.js';
import { Time } from '../utils/time.js';
Expand All @@ -18,7 +19,7 @@ export type PermissionsRequestOptions = {
signer: Signer;
};

export class PermissionsRequest extends Message<PermissionsRequestMessage> {
export class PermissionsRequest extends AbstractMessage<PermissionsRequestMessage> {

public static async parse(message: PermissionsRequestMessage): Promise<PermissionsRequest> {
await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor);
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/permissions-revoke.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Signer } from '../types/signer.js';
import type { PermissionsGrantMessage, PermissionsRevokeDescriptor, PermissionsRevokeMessage } from '../types/permissions-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { Time } from '../utils/time.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
Expand All @@ -12,7 +13,7 @@ export type PermissionsRevokeOptions = {
signer: Signer;
};

export class PermissionsRevoke extends Message<PermissionsRevokeMessage> {
export class PermissionsRevoke extends AbstractMessage<PermissionsRevokeMessage> {
public static async parse(message: PermissionsRevokeMessage): Promise<PermissionsRevoke> {
await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor);
Time.validateTimestamp(message.descriptor.messageTimestamp);
Expand Down
6 changes: 2 additions & 4 deletions src/interfaces/protocols-configure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Signer } from '../types/signer.js';
import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureDescriptor, ProtocolsConfigureMessage } from '../types/protocols-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { ProtocolActor } from '../types/protocols-types.js';
import { Time } from '../utils/time.js';
Expand All @@ -15,10 +16,7 @@ export type ProtocolsConfigureOptions = {
permissionsGrantId?: string;
};

export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
// JSON Schema guarantees presence of `authorization` which contains author DID
readonly author!: string;

export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessage> {
public static async parse(message: ProtocolsConfigureMessage): Promise<ProtocolsConfigure> {
Message.validateJsonSchema(message);
ProtocolsConfigure.validateProtocolDefinition(message.descriptor.definition);
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/protocols-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { MessageStore } from '../types/message-store.js';
import type { Signer } from '../types/signer.js';
import type { ProtocolsQueryDescriptor, ProtocolsQueryFilter, ProtocolsQueryMessage } from '../types/protocols-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { GrantAuthorization } from '../core/grant-authorization.js';
import { Message } from '../core/message.js';
import { removeUndefinedProperties } from '../utils/object.js';
Expand All @@ -19,7 +20,7 @@ export type ProtocolsQueryOptions = {
permissionsGrantId?: string;
};

export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
export class ProtocolsQuery extends AbstractMessage<ProtocolsQueryMessage> {

public static async parse(message: ProtocolsQueryMessage): Promise<ProtocolsQuery> {
if (message.authorization !== undefined) {
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/records-delete.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Signer } from '../types/signer.js';
import type { RecordsDeleteDescriptor, RecordsDeleteMessage } from '../types/records-types.js';

import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';

import { Time } from '../utils/time.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';

Expand All @@ -13,7 +13,7 @@ export type RecordsDeleteOptions = {
signer: Signer;
};

export class RecordsDelete extends Message<RecordsDeleteMessage> {
export class RecordsDelete extends AbstractMessage<RecordsDeleteMessage> {

public static async parse(message: RecordsDeleteMessage): Promise<RecordsDelete> {
await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor);
Expand Down
49 changes: 4 additions & 45 deletions src/interfaces/records-query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { DelegatedGrantMessage } from '../types/delegated-grant-message.js';
import type { Pagination } from '../types/message-types.js';
import type { Signer } from '../types/signer.js';
import type { DateSort, RecordsFilter, RecordsQueryDescriptor, RecordsQueryMessage } from '../types/records-types.js';
import type { GenericMessage, GenericSignaturePayload, Pagination } from '../types/message-types.js';

import { Jws } from '../utils/jws.js';
import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { Records } from '../utils/records.js';
import { removeUndefinedProperties } from '../utils/object.js';
Expand All @@ -29,49 +29,7 @@ export type RecordsQueryOptions = {
/**
* A class representing a RecordsQuery DWN message.
*/
export class RecordsQuery {
private _message: RecordsQueryMessage;
/**
* Valid JSON message representing this RecordsQuery.
*/
public get message(): RecordsQueryMessage {
return this._message as RecordsQueryMessage;
}

private _author: string | undefined;
/**
* DID of the logical author of this message.
* NOTE: we say "logical" author because a message can be signed by a delegate of the actual author,
* in which case the author DID would not be the same as the signer/delegate DID,
* but be the DID of the grantor (`grantedBy`) of the delegated grant presented.
*/
public get author(): string | undefined {
return this._author;
}

private _signaturePayload: GenericSignaturePayload | undefined;
/**
* Decoded payload of the signature of this message.
*/
public get signaturePayload(): GenericSignaturePayload | undefined {
return this._signaturePayload;
}

private constructor(message: RecordsQueryMessage) {
this._message = message;

if (message.authorization !== undefined) {
// if the message authorization contains author delegated grant, the author would be the grantor of the grant
// else the author would be the signer of the message
if (message.authorization.authorDelegatedGrant !== undefined) {
this._author = Message.getSigner(message.authorization.authorDelegatedGrant);
} else {
this._author = Message.getSigner(message as GenericMessage);
}

this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
}
}
export class RecordsQuery extends AbstractMessage<RecordsQueryMessage> {

public static async parse(message: RecordsQueryMessage): Promise<RecordsQuery> {
let signaturePayload;
Expand All @@ -95,6 +53,7 @@ export class RecordsQuery {
if (message.descriptor.filter.schema !== undefined) {
validateSchemaUrlNormalized(message.descriptor.filter.schema);
}

Time.validateTimestamp(message.descriptor.messageTimestamp);

return new RecordsQuery(message);
Expand Down

0 comments on commit 947a372

Please sign in to comment.