Skip to content

Commit

Permalink
#179 - removed 'target' property from message data model
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Jan 26, 2023
1 parent a59d519 commit ba253a4
Show file tree
Hide file tree
Showing 33 changed files with 328 additions and 462 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

Code Coverage
![Statements](https://img.shields.io/badge/statements-92.79%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-91.98%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-89.3%25-yellow.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-92.79%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-92.65%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-91.98%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-89.3%25-yellow.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-92.65%25-brightgreen.svg?style=flat)

## Introduction

Expand Down
19 changes: 7 additions & 12 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { BaseMessage } from './types.js';

import { CID } from 'multiformats';
import { Did } from '../did/did.js';
import { DidResolver } from '../did/did-resolver.js';
import { GeneralJws } from '../jose/jws/general/types.js';
import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js';
Expand All @@ -20,32 +19,29 @@ type AuthorizationPayloadConstraints = {
* @throws {Error} if auth fails
*/
export async function canonicalAuth(
tenant: string,
incomingMessage: Message,
didResolver: DidResolver
): Promise<void> {
await authenticate(incomingMessage.message.authorization, didResolver);
await authorize(incomingMessage);
await authorize(tenant, incomingMessage);
}

/**
* Validates the data integrity of the `authorization` property.
* NOTE: `target` and `descriptorCid` are both checked by default
* NOTE signature is not verified.
*/
export async function validateAuthorizationIntegrity(
message: BaseMessage,
authorizationPayloadConstraints?: AuthorizationPayloadConstraints
): Promise<{ target: string, descriptorCid: CID, [key: string]: any }> {
): Promise<{ descriptorCid: CID, [key: string]: any }> {

if (message.authorization.signatures.length !== 1) {
throw new Error('expected no more than 1 signature for authorization');
}

const payloadJson = GeneralJwsVerifier.decodePlainObjectPayload(message.authorization);
const { target, descriptorCid } = payloadJson;

// `target` validation
Did.validate(target);
const { descriptorCid } = payloadJson;

// `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message
const providedDescriptorCid = parseCid(descriptorCid); // parseCid throws an exception if parsing fails
Expand All @@ -57,7 +53,6 @@ export async function validateAuthorizationIntegrity(
// check to ensure that no other unexpected properties exist in payload.
const allowedProperties = authorizationPayloadConstraints?.allowedProperties ?? new Set();
const customProperties = { ...payloadJson };
delete customProperties.target;
delete customProperties.descriptorCid;
for (const propertyName in customProperties) {
{
Expand Down Expand Up @@ -89,9 +84,9 @@ export async function authenticate(jws: GeneralJws, didResolver: DidResolver): P
* Authorizes the incoming message.
* @throws {Error} if fails authentication
*/
export async function authorize(incomingMessage: Message): Promise<void> {
// if author/requester is the same as the target DID, we can directly grant access
if (incomingMessage.author === incomingMessage.target) {
export async function authorize(tenant: string, incomingMessage: Message): Promise<void> {
// if author/requester is the same as the target tenant, we can directly grant access
if (incomingMessage.author === tenant) {
return;
} else {
throw new Error('message failed authorization, permission grant check not yet implemented');
Expand Down
6 changes: 1 addition & 5 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@ export abstract class Message {

// commonly used properties for extra convenience;
readonly author: string;
readonly target: string;

constructor(message: BaseMessage) {
this.message = message;
this.authorizationPayload = GeneralJwsVerifier.decodePlainObjectPayload(message.authorization);

this.author = Message.getAuthor(message);
this.target = this.authorizationPayload.target;
}

/**
Expand Down Expand Up @@ -112,19 +110,17 @@ export abstract class Message {
/**
* Signs the provided message to be used an `authorization` property. Signed payload includes the CID of the message's descriptor by default
* along with any additional payload properties provided
* @param target - the logical DID where this message will be sent to
* @param descriptor - the message to sign
* @param signatureInput - the signature material to use (e.g. key and header data)
* @returns General JWS signature used as an `authorization` property.
*/
public static async signAsAuthorization(
target: string,
descriptor: Descriptor,
signatureInput: SignatureInput
): Promise<GeneralJws> {
const descriptorCid = await generateCid(descriptor);

const authPayload: BaseDecodedAuthorizationPayload = { target, descriptorCid: descriptorCid.toString() };
const authPayload: BaseDecodedAuthorizationPayload = { descriptorCid: descriptorCid.toString() };
const authPayloadStr = JSON.stringify(authPayload);
const authPayloadBytes = new TextEncoder().encode(authPayloadStr);

Expand Down
28 changes: 14 additions & 14 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ export class ProtocolAuthorization {
* @throws {Error} if authorization fails.
*/
public static async authorize(
tenant: string,
recordsWrite: RecordsWrite,
requesterDid: string,
messageStore: MessageStore
): Promise<void> {
// fetch the protocol definition
const protocolDefinition = await ProtocolAuthorization.fetchProtocolDefinition(recordsWrite, messageStore);
const protocolDefinition = await ProtocolAuthorization.fetchProtocolDefinition(tenant, recordsWrite, messageStore);

// fetch ancestor message chain
const ancestorMessageChain: RecordsWriteMessage[] = await ProtocolAuthorization.constructAncestorMessageChain(recordsWrite, messageStore);
const ancestorMessageChain: RecordsWriteMessage[] = await ProtocolAuthorization.constructAncestorMessageChain(tenant, recordsWrite, messageStore);

// record schema -> schema label map
const recordSchemaToLabelMap: Map<string, string> = new Map();
Expand All @@ -41,12 +42,11 @@ export class ProtocolAuthorization {
);

// verify the requester of the inbound message against allowed requester rule
ProtocolAuthorization.verifyAllowedRequester(
requesterDid, recordsWrite.target, inboundMessageRuleSet, ancestorMessageChain, recordSchemaToLabelMap
ProtocolAuthorization.verifyAllowedRequester(tenant, requesterDid, inboundMessageRuleSet, ancestorMessageChain, recordSchemaToLabelMap
);

// verify method invoked against the allowed actions
ProtocolAuthorization.verifyAllowedActions(requesterDid, recordsWrite, inboundMessageRuleSet);
ProtocolAuthorization.verifyAllowedActions(tenant, requesterDid, recordsWrite, inboundMessageRuleSet);

// verify allowed condition of the write
await ProtocolAuthorization.verifyActionCondition(recordsWrite, messageStore);
Expand All @@ -55,13 +55,13 @@ export class ProtocolAuthorization {
/**
* Fetches the protocol definition based on the protocol specified in the given message.
*/
private static async fetchProtocolDefinition(recordsWrite: RecordsWrite, messageStore: MessageStore): Promise<ProtocolDefinition> {
private static async fetchProtocolDefinition(tenant: string, recordsWrite: RecordsWrite, messageStore: MessageStore): Promise<ProtocolDefinition> {
// get the protocol URI
const protocolUri = recordsWrite.message.descriptor.protocol;

// fetch the corresponding protocol definition
const query = {
target : recordsWrite.target,
tenant,
method : DwnMethodName.ProtocolsConfigure,
protocol : protocolUri
};
Expand All @@ -79,7 +79,7 @@ export class ProtocolAuthorization {
* Constructs a chain of ancestor messages
* @returns the ancestor chain of messages where the first element is the root of the chain; returns empty array if no parent is specified.
*/
private static async constructAncestorMessageChain(recordsWrite: RecordsWrite, messageStore: MessageStore)
private static async constructAncestorMessageChain(tenant: string, recordsWrite: RecordsWrite, messageStore: MessageStore)
: Promise<RecordsWriteMessage[]> {
const ancestorMessageChain: RecordsWriteMessage[] = [];

Expand All @@ -91,7 +91,7 @@ export class ProtocolAuthorization {
while (currentParentId !== undefined) {
// fetch parent
const query = {
target : recordsWrite.target,
tenant,
method : DwnMethodName.RecordsWrite,
protocol,
contextId,
Expand Down Expand Up @@ -157,16 +157,16 @@ export class ProtocolAuthorization {
* @throws {Error} if requester not allowed.
*/
private static verifyAllowedRequester(
tenant: string,
requesterDid: string,
targetDid: string,
inboundMessageRuleSet: ProtocolRuleSet,
ancestorMessageChain: RecordsWriteMessage[],
recordSchemaToLabelMap: Map<string, string>
): void {
const allowRule = inboundMessageRuleSet.allow;
if (allowRule === undefined) {
// if no allow rule is defined, still allow if requester is the same as target, but throw otherwise
if (requesterDid !== targetDid) {
// if no allow rule is defined, still allow if requester is the same as target tenant, but throw otherwise
if (requesterDid !== tenant) {
throw new Error(`no allow rule defined for requester, ${requesterDid} is unauthorized`);
}
} else if (allowRule.anyone !== undefined) {
Expand All @@ -189,13 +189,13 @@ export class ProtocolAuthorization {
* Verifies the actions specified in the given message matches the allowed actions in the rule set.
* @throws {Error} if action not allowed.
*/
private static verifyAllowedActions(requesterDid: string, incomingMessage: Message, inboundMessageRuleSet: ProtocolRuleSet): void {
private static verifyAllowedActions(tenant: string, requesterDid: string, incomingMessage: Message, inboundMessageRuleSet: ProtocolRuleSet): void {
const allowRule = inboundMessageRuleSet.allow;
const incomingMessageMethod = incomingMessage.message.descriptor.method;

if (allowRule === undefined) {
// if no allow rule is defined, owner of DWN can do everything
if (requesterDid === incomingMessage.target) {
if (requesterDid === tenant) {
return;
} else {
throw new Error(`no allow rule defined for ${incomingMessageMethod}, ${requesterDid} is unauthorized`);
Expand Down
3 changes: 2 additions & 1 deletion src/core/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ type ResponseOptions = {
*/
export class Response {
// present ONLY if there is a general request-related issue
// e.g. malformed request, invalid target. `status` and `replies` are mutually exclusive
// e.g. malformed request.
// `status` and `replies` are mutually exclusive
status?: ResponseStatus;
// responses to individual messages provided within a request
replies?: MessageReply[];
Expand Down
1 change: 0 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export type BaseMessage = {
* Type of common decoded `authorization`property payload.
*/
export type BaseDecodedAuthorizationPayload = {
target: string;
descriptorCid: string;
};

Expand Down
19 changes: 12 additions & 7 deletions src/dwn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { MessageStoreLevel } from './store/message-store-level.js';
import { Request } from './core/request.js';
import { Response } from './core/response.js';

import { CollectionsInterface } from './interfaces/records/records-interface.js';
import { PermissionsInterface } from './interfaces/permissions/permissions-interface.js';
import { ProtocolsInterface } from './interfaces/protocols/protocols-interface.js';
import { RecordsInterface } from './interfaces/records/records-interface.js';

export class Dwn {
static methodHandlers: { [key:string]: MethodHandler } = {
...CollectionsInterface.methodHandlers,
...RecordsInterface.methodHandlers,
...PermissionsInterface.methodHandlers,
...ProtocolsInterface.methodHandlers
};
Expand Down Expand Up @@ -59,7 +59,11 @@ export class Dwn {
return this.messageStore.close();
}

async processRequest(rawRequest: Uint8Array): Promise<Response> {
/**
* Processes the given DWN request and returns with a DWN response.
* @param tenant The tenant DID to route the given request to.
*/
async processRequest(tenant: string, rawRequest: Uint8Array): Promise<Response> {
let request: RequestSchema;
try {
const requestString = Encoder.bytesToString(rawRequest);
Expand All @@ -81,7 +85,7 @@ export class Dwn {
for (const message of request.messages) {
let result;
try {
result = await this.processMessage(message);
result = await this.processMessage(tenant, message);
} catch (error) {
result = new MessageReply({
status: { code: 500, detail: error.message }
Expand All @@ -95,9 +99,10 @@ export class Dwn {
}

/**
* Processes the given DWN message.
* Processes the given DWN message and returns with a reply.
* @param tenant The tenant DID to route the given message to.
*/
async processMessage(rawMessage: any): Promise<MessageReply> {
async processMessage(tenant: string, rawMessage: any): Promise<MessageReply> {
const dwnMethod = rawMessage?.descriptor?.method;
if (dwnMethod === undefined) {
return new MessageReply({
Expand All @@ -116,7 +121,7 @@ export class Dwn {

const interfaceMethodHandler = Dwn.methodHandlers[dwnMethod];

const methodHandlerReply = await interfaceMethodHandler(rawMessage as BaseMessage, this.messageStore, this.DidResolver);
const methodHandlerReply = await interfaceMethodHandler(tenant, rawMessage as BaseMessage, this.messageStore, this.DidResolver);
return methodHandlerReply;
}
};
Expand Down
3 changes: 1 addition & 2 deletions src/interfaces/hooks/messages/hooks-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { DwnMethodName, Message } from '../../../core/message.js';
* Input to `HookssWrite.create()`.
*/
export type HooksWriteOptions = AuthCreateOptions & {
target: string,
dateCreated?: string,
/**
* leave as `undefined` for customer handler.
Expand Down Expand Up @@ -49,7 +48,7 @@ export class HooksWrite extends Message {

Message.validateJsonSchema({ descriptor, authorization: { } });

const authorization = await Message.signAsAuthorization(options.target, descriptor, options.signatureInput);
const authorization = await Message.signAsAuthorization(descriptor, options.signatureInput);
const message = { descriptor, authorization };

const hooksWrite = new HooksWrite(message);
Expand Down
13 changes: 9 additions & 4 deletions src/interfaces/permissions/handlers/permissions-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,31 @@ import { MessageReply } from '../../../core/message-reply.js';
import { PermissionsRequest } from '../messages/permissions-request.js';

export const handlePermissionsRequest: MethodHandler = async (
tenant,
message,
messageStore,
didResolver
): Promise<MessageReply> => {
const permissionRequest = await PermissionsRequest.parse(message as PermissionsRequestMessage);
const { author, target } = permissionRequest;
const { author } = permissionRequest;

if (permissionRequest.target !== permissionRequest.grantedBy && permissionRequest.target !== permissionRequest.grantedTo) {
if (tenant !== permissionRequest.grantedBy && tenant !== permissionRequest.grantedTo) {
return new MessageReply({
status: { code: 400, detail: 'grantedBy or grantedTo must be the targeted message recipient' }
});
}

await canonicalAuth(permissionRequest, didResolver);
await canonicalAuth(tenant, permissionRequest, didResolver);

if (author !== permissionRequest.grantedTo) {
throw new Error('grantee must be signer');
}

const index = { author, target, ... message.descriptor };
const index = {
tenant,
author,
... message.descriptor
};
await messageStore.put(message, index);

return new MessageReply({
Expand Down
Loading

0 comments on commit ba253a4

Please sign in to comment.