Skip to content

Commit

Permalink
Generalized roles so they can be specified under any context/sub-cont…
Browse files Browse the repository at this point in the history
…ext (#687)
  • Loading branch information
thehenrytsai authored Feb 15, 2024
1 parent 2ff6eb8 commit a842ef7
Show file tree
Hide file tree
Showing 11 changed files with 598 additions and 88 deletions.
4 changes: 2 additions & 2 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,25 @@ export enum DwnErrorCode {
ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath',
ProtocolAuthorizationInvalidSchema = 'ProtocolAuthorizationInvalidSchema',
ProtocolAuthorizationInvalidType = 'ProtocolAuthorizationInvalidType',
ProtocolAuthorizationMatchingRoleRecordNotFound = 'ProtocolAuthorizationMatchingRoleRecordNotFound',
ProtocolAuthorizationMaxSizeInvalid = 'ProtocolAuthorizationMaxSizeInvalid',
ProtocolAuthorizationMinSizeInvalid = 'ProtocolAuthorizationMinSizeInvalid',
ProtocolAuthorizationMissingContextId = 'ProtocolAuthorizationMissingContextId',
ProtocolAuthorizationMissingRole = 'ProtocolAuthorizationMissingRole',
ProtocolAuthorizationMissingRuleSet = 'ProtocolAuthorizationMissingRuleSet',
ProtocolAuthorizationParentlessIncorrectProtocolPath = 'ProtocolAuthorizationParentlessIncorrectProtocolPath',
ProtocolAuthorizationNotARole = 'ProtocolAuthorizationNotARole',
ProtocolAuthorizationParentNotFoundConstructingAncestorChain = 'ProtocolAuthorizationParentNotFoundConstructingAncestorChain',
ProtocolAuthorizationProtocolNotFound = 'ProtocolAuthorizationProtocolNotFound',
ProtocolAuthorizationQueryWithoutRole = 'ProtocolAuthorizationQueryWithoutRole',
ProtocolAuthorizationRoleMissingRecipient = 'ProtocolAuthorizationRoleMissingRecipient',
ProtocolsConfigureContextRoleAtProhibitedProtocolPath = 'ProtocolsConfigureContextRoleAtProhibitedProtocolPath',
ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath = 'ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath',
ProtocolsConfigureInvalidRole = 'ProtocolsConfigureInvalidRole',
ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize',
ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf',
ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed',
ProtocolsConfigureInvalidRecipientOfAction = 'ProtocolsConfigureInvalidRecipientOfAction',
ProtocolsConfigureQueryNotAllowed = 'ProtocolsConfigureQueryNotAllowed',
ProtocolsConfigureRecordNestingDepthExceeded = 'ProtocolsConfigureRecordNestingDepthExceeded',
ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized',
ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized',
RecordsDecryptNoMatchingKeyEncryptedFound = 'RecordsDecryptNoMatchingKeyEncryptedFound',
Expand Down
7 changes: 5 additions & 2 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ export class ProtocolAuthorization {

if (matchingMessages.length === 0) {
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationMissingRole,
DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound,
`No matching role record found for protocol path ${protocolRole}`
);
}
Expand Down Expand Up @@ -611,7 +611,10 @@ export class ProtocolAuthorization {
}

// No action rules were satisfied, author is not authorized
throw new DwnError(DwnErrorCode.ProtocolAuthorizationActionNotAllowed, `inbound message action not allowed for author`);
throw new DwnError(
DwnErrorCode.ProtocolAuthorizationActionNotAllowed,
`inbound message action ${incomingMessageMethod} not allowed for author ${incomingMessage.author}`
);
}

/**
Expand Down
54 changes: 36 additions & 18 deletions src/interfaces/protocols-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,38 +81,56 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
const rootRuleSet = definition.structure[rootRecordPath];

// gather $contextRoles
const contextRoles: string[] = [];
for (const childRecordType in rootRuleSet) {
if (childRecordType.startsWith('$')) {
continue;
}
const childRuleSet: ProtocolRuleSet = rootRuleSet[childRecordType];
if (childRuleSet.$contextRole) {
contextRoles.push(`${rootRecordPath}/${childRecordType}`);
}
const contextRoles = ProtocolsConfigure.fetchAllContextRolePathsRecursively(rootRecordPath, rootRuleSet, []);

ProtocolsConfigure.validateRuleSetRecursively(rootRuleSet, rootRecordPath, [...globalRoles, ...contextRoles]);
}
}

/**
* Parses the given rule set hierarchy to get all the context role protocol paths.
* @throws DwnError if the hierarchy depth goes beyond 10 levels.
*/
private static fetchAllContextRolePathsRecursively(recordProtocolPath: string, ruleSet: ProtocolRuleSet, contextRoles: string[]): string[] {
// Limit the depth of the record hierarchy to 10 levels
// There is opportunity to optimize here to avoid repeated string splitting
if (recordProtocolPath.split('/').length > 10) {
throw new DwnError(DwnErrorCode.ProtocolsConfigureRecordNestingDepthExceeded, 'Record nesting depth exceeded 10 levels.');
}

for (const recordType in ruleSet) {
// ignore non-nested-record properties
if (recordType.startsWith('$')) {
continue;
}

ProtocolsConfigure.validateRuleSet(rootRuleSet, rootRecordPath, [...globalRoles, ...contextRoles]);
const childRuleSet = ruleSet[recordType];
const childProtocolPath = `${recordProtocolPath}/${recordType}`;

// if this is a role record, add it to the list, else continue to traverse
if (childRuleSet.$contextRole) {
contextRoles.push(childProtocolPath);
} else {
ProtocolsConfigure.fetchAllContextRolePathsRecursively(childProtocolPath, childRuleSet, contextRoles);
}
}

return contextRoles;
}

/**
* Validates the given rule set structure then recursively validates its nested child rule sets.
*/
private static validateRuleSet(ruleSet: ProtocolRuleSet, protocolPath: string, roles: string[]): void {
private static validateRuleSetRecursively(ruleSet: ProtocolRuleSet, protocolPath: string, roles: string[]): void {
const depth = protocolPath.split('/').length;
if (ruleSet.$globalRole && depth !== 1) {
throw new DwnError(
DwnErrorCode.ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath,
`$globalRole is not allowed at protocol path (${protocolPath}). Only root records may set $globalRole true.`
);
} else if (ruleSet.$contextRole && depth !== 2) {
throw new DwnError(
DwnErrorCode.ProtocolsConfigureContextRoleAtProhibitedProtocolPath,
`$contextRole is not allowed at protocol path (${protocolPath}). Only second-level records may set $contextRole true.`
);
}

// Validate $actions in the rule set
if (ruleSet.$size !== undefined) {
const { min = 0, max } = ruleSet.$size;

Expand All @@ -124,7 +142,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
}
}

// Validate $actions in the ruleset
// Validate $actions in the rule set
const actions = ruleSet.$actions ?? [];
for (const action of actions) {
// Validate that all `role` properties contain protocol paths $globalRole or $contextRole records
Expand Down Expand Up @@ -175,7 +193,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
}
const rootRuleSet = ruleSet[recordType];
const nextProtocolPath = `${protocolPath}/${recordType}`;
ProtocolsConfigure.validateRuleSet(rootRuleSet, nextProtocolPath, roles);
ProtocolsConfigure.validateRuleSetRecursively(rootRuleSet, nextProtocolPath, roles);
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/handlers/records-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,7 +2385,7 @@ export function testRecordsQueryHandler(): void {
});
const chatQueryReply = await dwn.processMessage(alice.did, chatQuery.message) as RecordsQueryReply;
expect(chatQueryReply.status.code).to.eq(401);
expect(chatQueryReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatQueryReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});

it('rejects $contextRole authorized queries where the query author does not have a matching $contextRole', async () => {
Expand Down Expand Up @@ -2440,7 +2440,7 @@ export function testRecordsQueryHandler(): void {
});
const chatQueryReply = await dwn.processMessage(alice.did, chatQuery.message) as RecordsQueryReply;
expect(chatQueryReply.status.code).to.eq(401);
expect(chatQueryReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatQueryReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/handlers/records-read.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ export function testRecordsReadHandler(): void {
});
const chatReadReply = await dwn.processMessage(alice.did, readChatRecord.message);
expect(chatReadReply.status.code).to.equal(401);
expect(chatReadReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatReadReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});

it('uses a contextRole to authorize a read', async () => {
Expand Down Expand Up @@ -785,7 +785,7 @@ export function testRecordsReadHandler(): void {
});
const chatReadReply = await dwn.processMessage(alice.did, chatRead.message);
expect(chatReadReply.status.code).to.equal(401);
expect(chatReadReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatReadReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/handlers/records-subscribe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ export function testRecordsSubscribeHandler(): void {
});
const chatSubscribeReply = await dwn.processMessage(alice.did, chatSubscribe.message);
expect(chatSubscribeReply.status.code).to.eq(401);
expect(chatSubscribeReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatSubscribeReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
expect(chatSubscribeReply.subscription).to.not.exist;
});

Expand Down Expand Up @@ -694,7 +694,7 @@ export function testRecordsSubscribeHandler(): void {
});
const chatSubscribeReply = await dwn.processMessage(alice.did, chatSubscribe.message);
expect(chatSubscribeReply.status.code).to.eq(401);
expect(chatSubscribeReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatSubscribeReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
expect(chatSubscribeReply.subscription).to.not.exist;
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/handlers/records-write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,7 @@ export function testRecordsWriteHandler(): void {
});
const chatReply = await dwn.processMessage(alice.did, chatRecord.message, { dataStream: chatRecord.dataStream });
expect(chatReply.status.code).to.equal(401);
expect(chatReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});

it('uses a contextRole to authorize a write', async () => {
Expand Down Expand Up @@ -2270,7 +2270,7 @@ export function testRecordsWriteHandler(): void {
});
const chatRecordReply = await dwn.processMessage(alice.did, chatRecord.message, { dataStream: chatRecord.dataStream });
expect(chatRecordReply.status.code).to.equal(401);
expect(chatRecordReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMissingRole);
expect(chatRecordReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationMatchingRoleRecordNotFound);
});

it('rejects attempts to invoke an invalid path as a protocolRole', async () => {
Expand Down
99 changes: 41 additions & 58 deletions tests/interfaces/protocols-configure.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
import chaiAsPromised from 'chai-as-promised';
import chai, { expect } from 'chai';

import type { ProtocolsConfigureMessage } from '../../src/index.js';
import type { ProtocolsConfigureDescriptor, ProtocolsConfigureMessage } from '../../src/index.js';

import dexProtocolDefinition from '../vectors/protocol-definitions/dex.json' assert { type: 'json' };
import { DwnErrorCode } from '../../src/index.js';
import { Jws } from '../../src/utils/jws.js';
import { ProtocolsConfigure } from '../../src/interfaces/protocols-configure.js';
import { TestDataGenerator } from '../utils/test-data-generator.js';
import { Time } from '../../src/utils/time.js';
import { DwnErrorCode, DwnInterfaceName, DwnMethodName, Message } from '../../src/index.js';

chai.use(chaiAsPromised);

describe('ProtocolsConfigure', () => {
describe('parse()', () => {
it('should throw if protocol definitions has record nesting more than 10 level deep', async () => {
const definition = {
published : true,
protocol : 'http://example.com',
types : {
foo: {},
},
structure: { }
};

// create a record hierarchy with 11 levels of nesting
let currentLevel: any = definition.structure;
for (let i = 0; i < 11; i++) {
currentLevel.foo = { };
currentLevel = currentLevel.foo;
}

// we need to manually created an invalid protocol definition,
// because the SDK `create()` method will not allow us to create an invalid definition
const descriptor: ProtocolsConfigureDescriptor = {
interface : DwnInterfaceName.Protocols,
method : DwnMethodName.Configure,
messageTimestamp : Time.getCurrentTimestamp(),
definition
};

const alice = await TestDataGenerator.generatePersona();
const authorization = await Message.createAuthorization({
descriptor,
signer: Jws.createSigner(alice)
});
const message = { descriptor, authorization };

const parsePromise = ProtocolsConfigure.parse(message);
await expect(parsePromise).to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureRecordNestingDepthExceeded);
});
});

describe('create()', () => {
it('should use `messageTimestamp` as is if given', async () => {
const alice = await TestDataGenerator.generatePersona();
Expand Down Expand Up @@ -168,62 +207,6 @@ describe('ProtocolsConfigure', () => {
.to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureGlobalRoleAtProhibitedProtocolPath);
});

it('rejects protocol definitions with $contextRole at records that are not second-level records', async () => {
const alice = await TestDataGenerator.generatePersona();

// it rejects context roles too high in the structure
const definitionRootContextRole = {
published : true,
protocol : 'http://example.com',
types : {
root : {},
secondLevel : {}
},
structure: {
root: {
// $contextRole may only be set on second-level records, not root records
$contextRole: true,
}
}
};

const createProtocolsConfigurePromise = ProtocolsConfigure.create({
signer : Jws.createSigner(alice),
definition : definitionRootContextRole
});

await expect(createProtocolsConfigurePromise)
.to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureContextRoleAtProhibitedProtocolPath);

// it rejects contextRoles too nested in the structure
const definitionTooNestedContextRole = {
published : true,
protocol : 'http://example.com',
types : {
root : {},
secondLevel : {}
},
structure: {
root: {
secondLevel: {
thirdLevel: {
// $contextRole may only be set on second-level records, not third-level or lower records
$contextRole: true,
}
}
}
}
};

const createProtocolsConfigurePromise2 = ProtocolsConfigure.create({
signer : Jws.createSigner(alice),
definition : definitionTooNestedContextRole
});

await expect(createProtocolsConfigurePromise2)
.to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureContextRoleAtProhibitedProtocolPath);
});

it('rejects protocol definitions with `role` actions that contain invalid roles', async () => {
const definition = {
published : true,
Expand Down
Loading

0 comments on commit a842ef7

Please sign in to comment.