Skip to content

Commit

Permalink
Refactoring of grant related code for readability (#647)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Dec 13, 2023
1 parent 3bb47ca commit a6f95fb
Show file tree
Hide file tree
Showing 26 changed files with 191 additions and 126 deletions.
14 changes: 9 additions & 5 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AuthorizationModel } from '../types/message-types.js';
import type { DidResolver } from '../did/did-resolver.js';
import type { MessageInterface } from '../types/message-interface.js';
import type { AuthorizationModel, GenericMessage } from '../types/message-types.js';

import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js';
import { PermissionsGrant } from '../interfaces/permissions-grant.js';
Expand Down Expand Up @@ -30,14 +31,17 @@ export async function authenticate(authorizationModel: AuthorizationModel | unde
}

/**
* Authorizes the incoming message.
* @throws {Error} if fails authentication
* Authorizes owner authored message.
* @throws {DwnError} if fails authorization.
*/
export async function authorize(tenant: string, incomingMessage: { author: string | undefined }): Promise<void> {
export async function authorizeOwner(tenant: string, incomingMessage: MessageInterface<GenericMessage>): Promise<void> {
// if author is the same as the target tenant, we can directly grant access
if (incomingMessage.author === tenant) {
return;
} else {
throw new DwnError(DwnErrorCode.AuthorizationUnknownAuthor, 'message failed authorization. Only the tenant is authorized');
throw new DwnError(
DwnErrorCode.AuthorizationAuthorNotOwner,
`Message authored by ${incomingMessage.author}, not authored by expected owner ${tenant}.`
);
}
}
2 changes: 1 addition & 1 deletion src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export enum DwnErrorCode {
AuthenticateJwsMissing = 'AuthenticateJwsMissing',
AuthenticateDescriptorCidMismatch = 'AuthenticateDescriptorCidMismatch',
AuthenticationMoreThanOneSignatureNotSupported = 'AuthenticationMoreThanOneSignatureNotSupported',
AuthorizationUnknownAuthor = 'AuthorizationUnknownAuthor',
AuthorizationAuthorNotOwner = 'AuthorizationAuthorNotOwner',
AuthorizationNotGrantedToAuthor = 'AuthorizationNotGrantedToAuthor',
ComputeCidCodecNotSupported = 'ComputeCidCodecNotSupported',
ComputeCidMultihashNotSupported = 'ComputeCidMultihashNotSupported',
Expand Down
28 changes: 16 additions & 12 deletions src/core/grant-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,34 @@ import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.j
export class GrantAuthorization {

/**
* Performs PermissionsGrant-based authorization against the given message
* Does not validate grant `conditions` or `scope` beyond `interface` and `method`
* Performs base PermissionsGrant-based authorization against the given message:
* 1. Validates the `expectedGrantedToInGrant` and `expectedGrantedForInGrant` values against the actual values in given permissions grant.
* 2. Verifies that the incoming message is within the allowed time frame of the grant, and the grant has not been revoked.
* 3. Verifies that the `interface` and `method` grant scopes match the incoming message.
*
* NOTE: Does not validate grant `conditions` or `scope` beyond `interface` and `method`
*
* @param messageStore Used to check if the grant has been revoked.
* @throws {DwnError} if authorization fails
* @throws {DwnError} if validation fails
*/
public static async authorizeGenericMessage(input: {
tenant: string,
public static async performBaseValidation(input: {
incomingMessage: GenericMessage,
author: string,
expectedGrantedToInGrant: string,
expectedGrantedForInGrant: string,
permissionsGrantMessage: PermissionsGrantMessage,
messageStore: MessageStore,
}): Promise<void> {
const { tenant, incomingMessage, author, permissionsGrantMessage, messageStore } = input;
const { expectedGrantedForInGrant, incomingMessage, expectedGrantedToInGrant, permissionsGrantMessage, messageStore } = input;

const incomingMessageDescriptor = incomingMessage.descriptor;
const permissionsGrantId = await Message.getCid(permissionsGrantMessage);

const expectedGrantedToInGrant = author;
const expectedGrantedForInGrant = tenant;
GrantAuthorization.verifyExpectedGrantedToAndGrantedFor(expectedGrantedToInGrant, expectedGrantedForInGrant, permissionsGrantMessage);

// verify that grant is active during incomingMessage's timestamp
const grantedFor = expectedGrantedForInGrant; // renaming for better readability
await GrantAuthorization.verifyGrantActive(
tenant,
grantedFor,
incomingMessageDescriptor.messageTimestamp,
permissionsGrantMessage,
permissionsGrantId,
Expand Down Expand Up @@ -111,7 +115,7 @@ export class GrantAuthorization {
* @throws {DwnError} if incomingMessage has timestamp for a time in which the grant is not active.
*/
private static async verifyGrantActive(
tenant: string,
grantedFor: string,
incomingMessageTimestamp: string,
permissionsGrantMessage: PermissionsGrantMessage,
permissionsGrantId: string,
Expand All @@ -138,7 +142,7 @@ export class GrantAuthorization {
method : DwnMethodName.Revoke,
permissionsGrantId,
};
const { messages: revokes } = await messageStore.query(tenant, [query]);
const { messages: revokes } = await messageStore.query(grantedFor, [query]);
const oldestExistingRevoke = await Message.getOldestMessage(revokes);

if (oldestExistingRevoke !== undefined && oldestExistingRevoke.descriptor.messageTimestamp <= incomingMessageTimestamp) {
Expand Down
9 changes: 6 additions & 3 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,15 @@ export class Message {


/**
* Validates the structural integrity of the message signature given.
* NOTE: signature is not verified.
* Validates the structural integrity of the message signature given:
* 1. The message signature must contain exactly 1 signature
* 2. Passes JSON schema validation
* 3. The `descriptorCid` property matches the CID of the message descriptor
* NOTE: signature is NOT verified.
* @param payloadJsonSchemaKey The key to look up the JSON schema referenced in `compile-validators.js` and perform payload schema validation on.
* @returns the parsed JSON payload object if validation succeeds.
*/
public static async validateMessageSignatureIntegrity(
public static async validateSignatureStructure(
messageSignature: GeneralJws,
messageDescriptor: Descriptor,
payloadJsonSchemaKey: string = 'GenericSignaturePayload',
Expand Down
2 changes: 1 addition & 1 deletion src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ export class ProtocolAuthorization {
inboundMessageRuleSet: ProtocolRuleSet,
messageStore: MessageStore,
): Promise<void> {
const incomingRecordsWrite = incomingMessage as RecordsWrite;
const incomingRecordsWrite = incomingMessage;
if (!inboundMessageRuleSet.$globalRole && !inboundMessageRuleSet.$contextRole) {
return;
}
Expand Down
125 changes: 70 additions & 55 deletions src/core/records-grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MessageStore } from '../types/message-store.js';
import type { PermissionsGrantMessage } from '../types/permissions-types.js';
import type { RecordsPermissionScope } from '../types/permissions-grant-descriptor.js';
import type { PermissionsGrantMessage, RecordsPermissionsGrantMessage } from '../types/permissions-types.js';
import type { RecordsDeleteMessage, RecordsQueryMessage, RecordsReadMessage, RecordsWriteMessage } from '../types/records-types.js';

import { GrantAuthorization } from './grant-authorization.js';
Expand All @@ -11,71 +11,83 @@ export class RecordsGrantAuthorization {
/**
* Authorizes the given RecordsWrite in the scope of the DID given.
*/
public static async authorizeWrite(
tenant: string,
incomingMessage: RecordsWriteMessage,
author: string,
public static async authorizeWrite(input: {
recordsWriteMessage: RecordsWriteMessage,
expectedGrantedToInGrant: string,
expectedGrantedForInGrant: string,
permissionsGrantMessage: PermissionsGrantMessage,
messageStore: MessageStore,
): Promise<void> {
await GrantAuthorization.authorizeGenericMessage({
tenant,
incomingMessage,
author,
}): Promise<void> {
const {
recordsWriteMessage, expectedGrantedToInGrant, expectedGrantedForInGrant, permissionsGrantMessage, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: recordsWriteMessage,
expectedGrantedToInGrant,
expectedGrantedForInGrant,
permissionsGrantMessage,
messageStore
});

RecordsGrantAuthorization.verifyScope(incomingMessage, permissionsGrantMessage);
RecordsGrantAuthorization.verifyScope(recordsWriteMessage, permissionsGrantMessage as RecordsPermissionsGrantMessage);

RecordsGrantAuthorization.verifyConditions(incomingMessage, permissionsGrantMessage);
RecordsGrantAuthorization.verifyConditions(recordsWriteMessage, permissionsGrantMessage);
}

/**
* Authorizes the scope of a PermissionsGrant for RecordsRead.
* @param messageStore Used to check if the grant has been revoked.
* Authorizes a RecordsReadMessage using the given PermissionsGrant.
* @param messageStore Used to check if the given grant has been revoked.
*/
public static async authorizeRead(
tenant: string,
incomingMessage: RecordsReadMessage,
newestRecordsWriteMessage: RecordsWriteMessage,
author: string,
public static async authorizeRead(input: {
recordsReadMessage: RecordsReadMessage,
recordsWriteMessageToBeRead: RecordsWriteMessage,
expectedGrantedToInGrant: string,
expectedGrantedForInGrant: string,
permissionsGrantMessage: PermissionsGrantMessage,
messageStore: MessageStore,
): Promise<void> {
await GrantAuthorization.authorizeGenericMessage({
tenant,
incomingMessage,
author,
}): Promise<void> {
const {
expectedGrantedForInGrant, recordsReadMessage, recordsWriteMessageToBeRead, expectedGrantedToInGrant, permissionsGrantMessage, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: recordsReadMessage,
expectedGrantedToInGrant,
expectedGrantedForInGrant,
permissionsGrantMessage,
messageStore
});

RecordsGrantAuthorization.verifyScope(newestRecordsWriteMessage, permissionsGrantMessage);
RecordsGrantAuthorization.verifyScope(recordsWriteMessageToBeRead, permissionsGrantMessage as RecordsPermissionsGrantMessage);
}

/**
* Authorizes the scope of a PermissionsGrant for RecordsQuery.
* @param messageStore Used to check if the grant has been revoked.
*/
public static async authorizeQuery(
tenant: string,
incomingMessage: RecordsQueryMessage,
author: string,
public static async authorizeQuery(input: {
recordsQueryMessage: RecordsQueryMessage,
expectedGrantedToInGrant: string,
expectedGrantedForInGrant: string,
permissionsGrantMessage: PermissionsGrantMessage,
messageStore: MessageStore,
): Promise<void> {
await GrantAuthorization.authorizeGenericMessage({
tenant,
incomingMessage,
author,
}): Promise<void> {
const {
recordsQueryMessage, expectedGrantedToInGrant, expectedGrantedForInGrant, permissionsGrantMessage, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: recordsQueryMessage,
expectedGrantedToInGrant,
expectedGrantedForInGrant,
permissionsGrantMessage,
messageStore
});

// If the grant specifies a protocol, the query must specify the same protocol.
const protocolInGrant = (permissionsGrantMessage.descriptor.scope as RecordsPermissionScope).protocol;
const protocolInQuery = incomingMessage.descriptor.filter.protocol;
const protocolInGrant = (permissionsGrantMessage as RecordsPermissionsGrantMessage).descriptor.scope.protocol;
const protocolInQuery = recordsQueryMessage.descriptor.filter.protocol;
if (protocolInGrant !== undefined && protocolInQuery !== protocolInGrant) {
throw new DwnError(
DwnErrorCode.RecordsGrantAuthorizationQueryProtocolScopeMismatch,
Expand All @@ -88,24 +100,28 @@ export class RecordsGrantAuthorization {
* Authorizes the scope of a PermissionsGrant for RecordsDelete.
* @param messageStore Used to check if the grant has been revoked.
*/
public static async authorizeDelete(
tenant: string,
incomingDeleteMessage: RecordsDeleteMessage,
public static async authorizeDelete(input: {
recordsDeleteMessage: RecordsDeleteMessage,
recordsWriteToDelete: RecordsWriteMessage,
author: string,
expectedGrantedToInGrant: string,
expectedGrantedForInGrant: string,
permissionsGrantMessage: PermissionsGrantMessage,
messageStore: MessageStore,
): Promise<void> {
await GrantAuthorization.authorizeGenericMessage({
tenant,
incomingMessage: incomingDeleteMessage,
author,
}): Promise<void> {
const {
recordsDeleteMessage, recordsWriteToDelete, expectedGrantedToInGrant, expectedGrantedForInGrant, permissionsGrantMessage, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: recordsDeleteMessage,
expectedGrantedToInGrant,
expectedGrantedForInGrant,
permissionsGrantMessage,
messageStore
});

// If the grant specifies a protocol, the delete must be deleting a record with the same protocol.
const protocolInGrant = (permissionsGrantMessage.descriptor.scope as RecordsPermissionScope).protocol;
const protocolInGrant = (permissionsGrantMessage as RecordsPermissionsGrantMessage).descriptor.scope.protocol;
const protocolOfRecordToDelete = recordsWriteToDelete.descriptor.protocol;
if (protocolInGrant !== undefined && protocolOfRecordToDelete !== protocolInGrant) {
throw new DwnError(
Expand All @@ -121,25 +137,24 @@ export class RecordsGrantAuthorization {
*/
private static verifyScope(
recordsWriteMessage: RecordsWriteMessage,
permissionsGrantMessage: PermissionsGrantMessage,
permissionsGrantMessage: RecordsPermissionsGrantMessage,
): void {
const grantScope = permissionsGrantMessage.descriptor.scope as RecordsPermissionScope;

const grantScope = permissionsGrantMessage.descriptor.scope;
if (RecordsGrantAuthorization.isUnrestrictedScope(grantScope)) {
// scope has no restrictions beyond interface and method. Message is authorized to access any record.
return;
} else if (recordsWriteMessage.descriptor.protocol !== undefined) {
// authorization of protocol records must have grants that explicitly include the protocol
RecordsGrantAuthorization.authorizeProtocolRecord(recordsWriteMessage, grantScope);
RecordsGrantAuthorization.verifyProtocolRecordScope(recordsWriteMessage, grantScope);
} else {
RecordsGrantAuthorization.authorizeFlatRecord(recordsWriteMessage, grantScope);
RecordsGrantAuthorization.verifyFlatRecordScope(recordsWriteMessage, grantScope);
}
}

/**
* Authorizes a grant scope for a protocol record
* Verifies a protocol record against the scope of the given grant.
*/
private static authorizeProtocolRecord(
private static verifyProtocolRecordScope(
recordsWriteMessage: RecordsWriteMessage,
grantScope: RecordsPermissionScope
): void {
Expand Down Expand Up @@ -177,9 +192,9 @@ export class RecordsGrantAuthorization {
}

/**
* Authorizes a grant scope for a non-protocol record
* Verifies a non-protocol record against the scope of the given grant.
*/
private static authorizeFlatRecord(
private static verifyFlatRecordScope(
recordsWriteMessage: RecordsWriteMessage,
grantScope: RecordsPermissionScope
): void {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/events-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { EventsGetMessage, EventsGetReply } from '../types/event-types.js';

import { EventsGet } from '../interfaces/events-get.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { authenticate, authorize } from '../core/auth.js';
import { authenticate, authorizeOwner } from '../core/auth.js';

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

Expand All @@ -24,7 +24,7 @@ export class EventsGetHandler implements MethodHandler {

try {
await authenticate(message.authorization, this.didResolver);
await authorize(tenant, eventsGet);
await authorizeOwner(tenant, eventsGet);
} catch (e) {
return messageReplyFromError(e, 401);
}
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/events-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { EventsQueryMessage, EventsQueryReply } from '../types/event-types.

import { EventsQuery } from '../interfaces/events-query.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { authenticate, authorize } from '../core/auth.js';
import { authenticate, authorizeOwner } from '../core/auth.js';


export class EventsQueryHandler implements MethodHandler {
Expand All @@ -26,7 +26,7 @@ export class EventsQueryHandler implements MethodHandler {

try {
await authenticate(message.authorization, this.didResolver);
await authorize(tenant, eventsQuery);
await authorizeOwner(tenant, eventsQuery);
} catch (e) {
return messageReplyFromError(e, 401);
}
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/messages-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { MessagesGetMessage, MessagesGetReply, MessagesGetReplyEntry } from

import { messageReplyFromError } from '../core/message-reply.js';
import { MessagesGet } from '../interfaces/messages-get.js';
import { authenticate, authorize } from '../core/auth.js';
import { authenticate, authorizeOwner } from '../core/auth.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';

type HandleArgs = { tenant: string, message: MessagesGetMessage };
Expand All @@ -26,7 +26,7 @@ export class MessagesGetHandler implements MethodHandler {

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

0 comments on commit a6f95fb

Please sign in to comment.