Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol Interface Permissions #803

Merged
merged 13 commits into from
Sep 10, 2024
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
84 changes: 84 additions & 0 deletions src/core/protocols-grant-authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 RecordsWrite in the scope of the DID given.
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
*/
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);
}

public static async authorizeQuery(input: {
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
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 record against the scope of the given grant.
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
*/
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
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
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