Skip to content

Commit

Permalink
Protocol Interface Permissions (#803)
Browse files Browse the repository at this point in the history
- add delegateGrant ability to `ProtocolConfigure`
- Allow `ProtocolPermissionScope` to be scoped down to a specific protocol
- move getAuthor helper from `Record` util class to `Message` core class.

When DWAs request permissions, they will now be issued a `ProtocolsQuery` permission scoped to the protocol they are being authorized, as well as optionally a `ProtocolsConfigure` for that protocol.

Satisfies:
#801
#802
  • Loading branch information
LiranCohen authored Sep 10, 2024
1 parent a5d66bf commit 080359c
Show file tree
Hide file tree
Showing 23 changed files with 651 additions and 111 deletions.
2 changes: 1 addition & 1 deletion json-schemas/interface-methods/protocols-configure.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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
3 changes: 3 additions & 0 deletions json-schemas/permissions/permissions-definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-subscribe-scope"
},
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-configure-scope"
},
{
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-query-scope"
},
Expand Down
22 changes: 22 additions & 0 deletions json-schemas/permissions/scopes.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@
}
}
},
"protocols-configure-scope": {
"type": "object",
"additionalProperties": false,
"required": [
"interface",
"method"
],
"properties": {
"interface": {
"const": "Protocols"
},
"method": {
"const": "Configure"
},
"protocol": {
"type": "string"
}
}
},
"protocols-query-scope": {
"type": "object",
"additionalProperties": false,
Expand All @@ -73,6 +92,9 @@
},
"method": {
"const": "Query"
},
"protocol": {
"type": "string"
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions src/core/abstract-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export abstract class AbstractMessage<M extends GenericMessage> implements Messa
return this._signaturePayload;
}

/**
* If this message is signed by an author-delegate.
*/
public get isSignedByAuthorDelegate(): boolean {
return Message.isSignedByAuthorDelegate(this._message);
}

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

Expand Down
21 changes: 2 additions & 19 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AuthorizationModel } from '../types/message-types.js';
import type { DidResolver } from '@web5/dids';
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 { RecordsWrite } from '../interfaces/records-write.js';
Expand Down Expand Up @@ -34,20 +33,4 @@ export async function authenticate(authorizationModel: AuthorizationModel | unde
const ownerDelegatedGrant = await RecordsWrite.parse(authorizationModel.ownerDelegatedGrant);
await GeneralJwsVerifier.verifySignatures(ownerDelegatedGrant.message.authorization.signature, didResolver);
}
}

/**
* Authorizes owner authored message.
* @throws {DwnError} if fails authorization.
*/
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.AuthorizationAuthorNotOwner,
`Message authored by ${incomingMessage.author}, not authored by expected owner ${tenant}.`
);
}
}
}
4 changes: 3 additions & 1 deletion src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export enum DwnErrorCode {
AuthenticateJwsMissing = 'AuthenticateJwsMissing',
AuthenticateDescriptorCidMismatch = 'AuthenticateDescriptorCidMismatch',
AuthenticationMoreThanOneSignatureNotSupported = 'AuthenticationMoreThanOneSignatureNotSupported',
AuthorizationAuthorNotOwner = 'AuthorizationAuthorNotOwner',
AuthorizationNotGrantedToAuthor = 'AuthorizationNotGrantedToAuthor',
ComputeCidCodecNotSupported = 'ComputeCidCodecNotSupported',
ComputeCidMultihashNotSupported = 'ComputeCidMultihashNotSupported',
Expand Down Expand Up @@ -79,6 +78,7 @@ export enum DwnErrorCode {
ProtocolAuthorizationProtocolNotFound = 'ProtocolAuthorizationProtocolNotFound',
ProtocolAuthorizationRoleMissingRecipient = 'ProtocolAuthorizationRoleMissingRecipient',
ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema',
ProtocolsConfigureAuthorizationFailed = 'ProtocolsConfigureAuthorizationFailed',
ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet',
ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet',
ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize',
Expand All @@ -91,6 +91,8 @@ export enum DwnErrorCode {
ProtocolsConfigureInvalidTagSchema = 'ProtocolsConfigureInvalidTagSchema',
ProtocolsConfigureRecordNestingDepthExceeded = 'ProtocolsConfigureRecordNestingDepthExceeded',
ProtocolsConfigureRoleDoesNotExistAtGivenPath = 'ProtocolsConfigureRoleDoesNotExistAtGivenPath',
ProtocolsGrantAuthorizationQueryProtocolScopeMismatch = 'ProtocolsGrantAuthorizationQueryProtocolScopeMismatch',
ProtocolsGrantAuthorizationScopeProtocolMismatch = 'ProtocolsGrantAuthorizationScopeProtocolMismatch',
ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized',
RecordsAuthorDelegatedGrantAndIdExistenceMismatch = 'RecordsAuthorDelegatedGrantAndIdExistenceMismatch',
RecordsAuthorDelegatedGrantCidMismatch = 'RecordsAuthorDelegatedGrantCidMismatch',
Expand Down
19 changes: 19 additions & 0 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ import { DwnError, DwnErrorCode } from './dwn-error.js';
* A class containing utility methods for working with DWN messages.
*/
export class Message {

/**
* Gets the DID of the author of the given message.
*/
public static getAuthor(message: GenericMessage): string | undefined {
if (message.authorization === undefined) {
return undefined;
}

let author;
if (message.authorization.authorDelegatedGrant !== undefined) {
author = Message.getSigner(message.authorization.authorDelegatedGrant);
} else {
author = Message.getSigner(message);
}

return author;
}

/**
* Validates the given message against the corresponding JSON schema.
* @throws {Error} if fails validation.
Expand Down
88 changes: 88 additions & 0 deletions src/core/protocols-grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { MessageStore } from '../types/message-store.js';
import type { PermissionGrant } from '../protocols/permission-grant.js';
import type { ProtocolPermissionScope } from '../types/permission-types.js';
import type { ProtocolsConfigureMessage, ProtocolsQueryMessage } from '../types/protocols-types.js';

import { GrantAuthorization } from './grant-authorization.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';

export class ProtocolsGrantAuthorization {
/**
* Authorizes the given ProtocolsConfigure in the scope of the DID given.
*/
public static async authorizeConfigure(input: {
protocolsConfigureMessage: ProtocolsConfigureMessage,
expectedGrantor: string,
expectedGrantee: string,
permissionGrant: PermissionGrant,
messageStore: MessageStore,
}): Promise<void> {
const {
protocolsConfigureMessage, expectedGrantor, expectedGrantee, permissionGrant, messageStore
} = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: protocolsConfigureMessage,
expectedGrantor,
expectedGrantee,
permissionGrant,
messageStore
});

ProtocolsGrantAuthorization.verifyScope(protocolsConfigureMessage, permissionGrant.scope as ProtocolPermissionScope);
}

/**
* Authorizes the scope of a permission grant for a ProtocolsQuery message.
* @param messageStore Used to check if the grant has been revoked.
*/
public static async authorizeQuery(input: {
expectedGrantor: string,
expectedGrantee: string,
incomingMessage: ProtocolsQueryMessage;
permissionGrant: PermissionGrant;
messageStore: MessageStore;
}): Promise<void> {
const { expectedGrantee, expectedGrantor, incomingMessage, permissionGrant, messageStore } = input;

await GrantAuthorization.performBaseValidation({
incomingMessage: incomingMessage,
expectedGrantor,
expectedGrantee,
permissionGrant,
messageStore
});

// If the grant specifies a protocol, the query must specify the same protocol.
const permissionScope = permissionGrant.scope as ProtocolPermissionScope;
const protocolInGrant = permissionScope.protocol;
const protocolInMessage = incomingMessage.descriptor.filter?.protocol;
if (protocolInGrant !== undefined && protocolInMessage !== protocolInGrant) {
throw new DwnError(
DwnErrorCode.ProtocolsGrantAuthorizationQueryProtocolScopeMismatch,
`Grant protocol scope ${protocolInGrant} does not match protocol in message ${protocolInMessage}`
);
}
}

/**
* Verifies a ProtocolsConfigure against the scope of the given grant.
*/
private static verifyScope(
protocolsConfigureMessage: ProtocolsConfigureMessage,
grantScope: ProtocolPermissionScope
): void {

// if the grant scope does not specify a protocol, then it is am unrestricted grant
if (grantScope.protocol === undefined) {
return;
}

if (grantScope.protocol !== protocolsConfigureMessage.descriptor.definition.protocol) {
throw new DwnError(
DwnErrorCode.ProtocolsGrantAuthorizationScopeProtocolMismatch,
`Grant scope specifies different protocol than what appears in the configure message.`
);
}
}
}
29 changes: 27 additions & 2 deletions src/handlers/protocols-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import type { MessageStore } from '../types//message-store.js';
import type { MethodHandler } from '../types/method-handler.js';
import type { ProtocolsConfigureMessage } from '../types/protocols-types.js';

import { authenticate } from '../core/auth.js';
import { Message } from '../core/message.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { ProtocolsConfigure } from '../interfaces/protocols-configure.js';
import { authenticate, authorizeOwner } from '../core/auth.js';
import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';

export class ProtocolsConfigureHandler implements MethodHandler {
Expand All @@ -35,7 +38,7 @@ export class ProtocolsConfigureHandler implements MethodHandler {
// authentication & authorization
try {
await authenticate(message.authorization, this.didResolver);
await authorizeOwner(tenant, protocolsConfigure);
await ProtocolsConfigureHandler.authorizeProtocolsConfigure(tenant, protocolsConfigure, this.messageStore);
} catch (e) {
return messageReplyFromError(e, 401);
}
Expand Down Expand Up @@ -109,4 +112,26 @@ export class ProtocolsConfigureHandler implements MethodHandler {

return indexes;
}

private static async authorizeProtocolsConfigure(tenant: string, protocolConfigure: ProtocolsConfigure, messageStore: MessageStore): Promise<void> {

if (protocolConfigure.isSignedByAuthorDelegate) {
await protocolConfigure.authorizeAuthorDelegate(messageStore);
}

if (protocolConfigure.author === tenant) {
return;
} else if (protocolConfigure.author !== undefined && protocolConfigure.signaturePayload!.permissionGrantId !== undefined) {
const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, protocolConfigure.signaturePayload!.permissionGrantId);
await ProtocolsGrantAuthorization.authorizeConfigure({
protocolsConfigureMessage : protocolConfigure.message,
expectedGrantor : tenant,
expectedGrantee : protocolConfigure.author,
permissionGrant,
messageStore
});
} else {
throw new DwnError(DwnErrorCode.ProtocolsConfigureAuthorizationFailed, 'message failed authorization');
}
}
}
4 changes: 3 additions & 1 deletion src/handlers/protocols-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export class ProtocolsQueryHandler implements MethodHandler {

// return public ProtocolsConfigures if query fails with a certain authentication or authorization code
if (error.code === DwnErrorCode.AuthenticateJwsMissing || // unauthenticated
error.code === DwnErrorCode.ProtocolsQueryUnauthorized) {
error.code === DwnErrorCode.ProtocolsQueryUnauthorized ||
error.code === DwnErrorCode.ProtocolsGrantAuthorizationQueryProtocolScopeMismatch
) {

const entries: ProtocolsConfigureMessage[] = await this.fetchPublishedProtocolsConfigure(tenant, protocolsQuery);
return {
Expand Down
24 changes: 24 additions & 0 deletions src/interfaces/protocols-configure.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js';
import type { MessageStore } from '../types/message-store.js';
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 Ajv from 'ajv/dist/2020.js';
import { Message } from '../core/message.js';
import { PermissionGrant } from '../protocols/permission-grant.js';
import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js';
import { Time } from '../utils/time.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
Expand All @@ -14,6 +18,10 @@ export type ProtocolsConfigureOptions = {
messageTimestamp?: string;
definition: ProtocolDefinition;
signer: Signer;
/**
* The delegated grant invoked to sign on behalf of the logical author, which is the grantor of the delegated grant.
*/
delegatedGrant?: DataEncodedRecordsWriteMessage;
permissionGrantId?: string;
};

Expand All @@ -38,6 +46,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
const authorization = await Message.createAuthorization({
descriptor,
signer : options.signer,
delegatedGrant : options.delegatedGrant,
permissionGrantId : options.permissionGrantId
});
const message = { descriptor, authorization };
Expand All @@ -49,6 +58,21 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
return protocolsConfigure;
}

/**
* Authorizes the author-delegate who signed this message.
* @param messageStore Used to check if the grant has been revoked.
*/
public async authorizeAuthorDelegate(messageStore: MessageStore): Promise<void> {
const delegatedGrant = await PermissionGrant.parse(this.message.authorization.authorDelegatedGrant!);
await ProtocolsGrantAuthorization.authorizeConfigure({
protocolsConfigureMessage : this.message,
expectedGrantor : this.author!,
expectedGrantee : this.signer!,
permissionGrant : delegatedGrant,
messageStore
});
}

/**
* Performs validation on the given protocol definition that are not easy to do using a JSON schema.
*/
Expand Down
9 changes: 4 additions & 5 deletions src/interfaces/protocols-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ 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 { PermissionsProtocol } from '../protocols/permissions.js';
import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js';
import { removeUndefinedProperties } from '../utils/object.js';
import { Time } from '../utils/time.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../utils/url.js';

import { DwnError, DwnErrorCode } from '../core/dwn-error.js';

export type ProtocolsQueryOptions = {
messageTimestamp?: string;
filter?: ProtocolsQueryFilter,
Expand Down Expand Up @@ -80,10 +79,10 @@ export class ProtocolsQuery extends AbstractMessage<ProtocolsQueryMessage> {
return;
} else if (this.author !== undefined && this.signaturePayload!.permissionGrantId) {
const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, this.signaturePayload!.permissionGrantId);
await GrantAuthorization.performBaseValidation({
incomingMessage : this.message,
await ProtocolsGrantAuthorization.authorizeQuery({
expectedGrantor : tenant,
expectedGrantee : this.author,
incomingMessage : this.message,
permissionGrant,
messageStore
});
Expand Down
Loading

0 comments on commit 080359c

Please sign in to comment.