Skip to content

Commit

Permalink
Authorize RecordsRead with globalRole
Browse files Browse the repository at this point in the history
  • Loading branch information
Diane Huxley committed Sep 21, 2023
1 parent f75ff98 commit 7f22727
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Decentralized Web Node (DWN) SDK <!-- omit in toc -->

Code Coverage
![Statements](https://img.shields.io/badge/statements-97.77%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.1%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-94.25%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.77%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-97.77%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.03%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-94.28%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.77%25-brightgreen.svg?style=flat)

- [Introduction](#introduction)
- [Installation](#installation)
Expand Down
3 changes: 3 additions & 0 deletions json-schemas/interface-methods/records-read.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
},
"filter": {
"$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json"
},
"protocolRole": {
"type": "string"
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ export enum DwnErrorCode {
PrivateKeySignerUnableToDeduceKeyId = 'PrivateKeySignerUnableToDeduceKeyId',
PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve',
ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed',
ProtocolsAuthorizationDuplicateGlobalRoleRecipient = 'ProtocolsAuthorizationDuplicateGlobalRoleRecipient',
ProtocolAuthorizationDuplicateGlobalRoleRecipient = 'ProtocolAuthorizationDuplicateGlobalRoleRecipient',
ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat',
ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath',
ProtocolAuthorizationInvalidSchema = 'ProtocolAuthorizationInvalidSchema',
ProtocolAuthorizationInvalidType = 'ProtocolAuthorizationInvalidType',
ProtocolAuthorizationRoleInvokedByAuthorless = 'ProtocolAuthorizationRoleInvokedByAuthorless',
ProtocolAuthorizationMissingRole = 'ProtocolAuthorizationMissingRole',
ProtocolAuthorizationMissingRuleSet = 'ProtocolAuthorizationMissingRuleSet',
ProtocolAuthorizationNotARole = 'ProtocolAuthorizationNotARole',
ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath = 'ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath',
ProtocolsConfigureInvalidRole = 'ProtocolsConfigureInvalidRole',
ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized',
Expand Down
125 changes: 105 additions & 20 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ export class ProtocolAuthorization {
protocolDefinition,
);

// If the incoming message has `protocolRole` in the descriptor, validate the invoked role
await ProtocolAuthorization.verifyInvokedRole(
tenant,
incomingMessage,
recordsWrite,
protocolDefinition,
messageStore,
);

// verify method invoked against the allowed actions
await ProtocolAuthorization.verifyAllowedActions(
tenant,
Expand All @@ -68,6 +77,7 @@ export class ProtocolAuthorization {
messageStore,
);

// If the incoming message is writing a $globalRole record, validate that the recipient is unique
await ProtocolAuthorization.verifyUniqueRoleRecipient(
tenant,
incomingMessage,
Expand Down Expand Up @@ -160,26 +170,13 @@ export class ProtocolAuthorization {
protocolDefinition: ProtocolDefinition,
): ProtocolRuleSet {
const protocolPath = recordsWrite.message.descriptor.protocolPath!;
const protocolPathArray = protocolPath.split('/');

// traverse rule sets using protocolPath
let currentRuleSet: ProtocolRuleSet = protocolDefinition.structure;
let i = 0;
while (i < protocolPathArray.length) {
const currentTypeName = protocolPathArray[i];
const nextRuleSet: ProtocolRuleSet | undefined = currentRuleSet[currentTypeName];

if (nextRuleSet === undefined) {
const partialProtocolPath = protocolPathArray.slice(0, i + 1).join('/');
throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRuleSet,
`No rule set defined for protocolPath ${partialProtocolPath}`);
}

currentRuleSet = nextRuleSet;
i++;
const ruleSet = ProtocolAuthorization.getRuleSetAtProtocolPath(protocolPath, protocolDefinition);
if (ruleSet === undefined) {
throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRuleSet,
`No rule set defined for protocolPath ${protocolPath}`);
}

return currentRuleSet;
return ruleSet;
}

/**
Expand Down Expand Up @@ -264,6 +261,61 @@ export class ProtocolAuthorization {
}
}

/**
* Check if the incoming message is invoking a role. If so, validate the invoked role.
*/
private static async verifyInvokedRole(
tenant: string,
incomingMessage: RecordsRead | RecordsWrite,
recordsWrite: RecordsWrite,
protocolDefinition: ProtocolDefinition,
messageStore: MessageStore,
): Promise<void> {
// Currently only RecordsReads may invoke a role
if (incomingMessage.message.descriptor.method !== DwnMethodName.Read) {
return;
}

const protocolRole = (incomingMessage as RecordsRead).message.descriptor.protocolRole;

// Only verify role if there is a role being invoked
if (protocolRole === undefined) {
return;
}

if (incomingMessage.author === undefined) {
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationRoleInvokedByAuthorless,
'Authorless messages may not invoke a protocolRole'
);
}

const roleRuleSet = ProtocolAuthorization.getRuleSetAtProtocolPath(protocolRole, protocolDefinition);
if (roleRuleSet?.$globalRole === undefined) {
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationNotARole,
`Protocol path ${protocolRole} is not a valid protocolRole`
);
}

const roleRecordFilter = {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
protocol : recordsWrite.message.descriptor.protocol!,
protocolPath : protocolRole,
recipient : incomingMessage.author!,
isLatestBaseState : true,
};
const { messages: matchingMessages } = await messageStore.query(tenant, [roleRecordFilter]);

if (matchingMessages.length === 0) {
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationMissingRole,
`No role record found for protocol path ${protocolRole}`
);
}
}

/**
* Verifies the actions specified in the given message matches the allowed actions in the rule set.
* @throws {Error} if action not allowed.
Expand Down Expand Up @@ -301,12 +353,26 @@ export class ProtocolAuthorization {
throw new Error(`no action rule defined for ${incomingMessage.message.descriptor.method}, ${author} is unauthorized`);
}

// Get role being invoked. Currently only Reads support role-based authorization
let invokedRole: string | undefined;
if (incomingMessage.message.descriptor.method === DwnMethodName.Read) {
invokedRole = incomingMessage.message.descriptor.protocolRole;
}

for (const actionRule of actionRules) {
if (actionRule.can !== inboundMessageAction) {
continue;
}

if (actionRule.who === ProtocolActor.Anyone) {
if (invokedRole !== undefined) {
// When a protocol role is being invoked, we require that there is a matching `role` rule.
if (actionRule.role === invokedRole) {
// role is successfully invoked
return;
} else {
continue;
}
} else if (actionRule.who === ProtocolActor.Anyone) {
return;
} else if (author === undefined) {
continue;
Expand Down Expand Up @@ -359,7 +425,7 @@ export class ProtocolAuthorization {
);
if (matchingRecordsExceptIncomingRecordId.length > 0) {
throw new DwnError(
DwnErrorCode.ProtocolsAuthorizationDuplicateGlobalRoleRecipient,
DwnErrorCode.ProtocolAuthorizationDuplicateGlobalRoleRecipient,
`DID '${recipient}' is already recipient of a $globalRole record at protocol path '${protocolPath}`
);
}
Expand Down Expand Up @@ -393,6 +459,25 @@ export class ProtocolAuthorization {
}
}

private static getRuleSetAtProtocolPath(protocolPath: string, protocolDefinition: ProtocolDefinition): ProtocolRuleSet | undefined {
const protocolPathArray = protocolPath.split('/');
let currentRuleSet: ProtocolRuleSet = protocolDefinition.structure;
let i = 0;
while (i < protocolPathArray.length) {
const currentTypeName = protocolPathArray[i];
const nextRuleSet: ProtocolRuleSet | undefined = currentRuleSet[currentTypeName];

if (nextRuleSet === undefined) {
return undefined;
}

currentRuleSet = nextRuleSet;
i++;
}

return currentRuleSet;
}

/**
* Checks if there is a RecordsWriteMessage in the ancestor chain that matches the protocolPath in given ProtocolActionRule.
* Assumes that the actionRule authorizes either recipient or author, but not 'anyone'.
Expand Down
8 changes: 7 additions & 1 deletion src/interfaces/records-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type RecordsReadOptions = {
date?: string;
authorizationSigner?: Signer;
permissionsGrantId?: string;
/**
* Used when authorizing protocol records.
* The protocol path to a $globalRole record whose recipient is the author of this RecordsRead
*/
protocolRole?: string;
};

export class RecordsRead extends Message<RecordsReadMessage> {
Expand Down Expand Up @@ -46,7 +51,8 @@ export class RecordsRead extends Message<RecordsReadMessage> {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Read,
filter : Records.normalizeFilter(filter),
messageTimestamp : options.date ?? currentTime
messageTimestamp : options.date ?? currentTime,
protocolRole : options.protocolRole,
};

removeUndefinedProperties(descriptor);
Expand Down
1 change: 1 addition & 0 deletions src/types/records-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export type RecordsReadDescriptor = {
method: DwnMethodName.Read;
filter: RecordsFilter;
messageTimestamp: string;
protocolRole?: string;
};

export type RecordsDeleteMessage = GenericMessage & {
Expand Down
Loading

0 comments on commit 7f22727

Please sign in to comment.