Skip to content

Commit

Permalink
Introduced first-class Permissions protocol (#713)
Browse files Browse the repository at this point in the history
This will replace the entire `Permissions` interface.

This PR only contains the support of permission life-cycle management
through Request, Grant, and Revocation records.

The actual replacing/swapping out the existing logic that depends on
`Permissions` messages will come in the next PR.
  • Loading branch information
thehenrytsai committed Mar 27, 2024
1 parent 1e9cd85 commit ee68791
Show file tree
Hide file tree
Showing 12 changed files with 604 additions and 8 deletions.
31 changes: 31 additions & 0 deletions json-schemas/permission-grant.json
@@ -0,0 +1,31 @@
{
"$id": "https://identity.foundation/dwn/json-schemas/permission-grant.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"dateExpires",
"scope"
],
"properties": {
"dateExpires": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time"
},
"description": {
"type": "string"
},
"delegated": {
"type": "boolean"
},
"requestId": {
"description": "CID of an associated permission request DWN message",
"type": "string"
},
"scope": {
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/definitions/scope"
},
"conditions": {
"$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/definitions/conditions"
}
}
}
6 changes: 6 additions & 0 deletions src/core/protocol-authorization.ts
Expand Up @@ -8,6 +8,7 @@ import type { RecordsWriteMessage } from '../types/records-types.js';
import type { ProtocolActionRule, ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage, ProtocolType, ProtocolTypes } from '../types/protocols-types.js';

import { FilterUtility } from '../utils/filter.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { Records } from '../utils/records.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
Expand Down Expand Up @@ -250,6 +251,11 @@ export class ProtocolAuthorization {
protocolUri: string,
messageStore: MessageStore
): Promise<ProtocolDefinition> {
// if first-class protocol, return the definition from const object directly without going to data store
if (protocolUri === PermissionsProtocol.uri) {
return PermissionsProtocol.definition;
}

// fetch the corresponding protocol definition
const query: Filter = {
interface : DwnInterfaceName.Protocols,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -37,6 +37,7 @@ export { UnionMessageReply } from './core/message-reply.js';
export { MessageStore, MessageStoreOptions } from './types/message-store.js';
export { PermissionsGrant, PermissionsGrantOptions } from './interfaces/permissions-grant.js';
export { PermissionsRequest, PermissionsRequestOptions } from './interfaces/permissions-request.js';
export { PermissionsProtocol } from './protocols/permissions.js';
export { PermissionsRevoke, PermissionsRevokeOptions } from './interfaces/permissions-revoke.js';
export { PrivateKeySigner } from './utils/private-key-signer.js';
export { Protocols } from './utils/protocols.js';
Expand Down
9 changes: 5 additions & 4 deletions src/interfaces/records-write.ts
Expand Up @@ -57,12 +57,12 @@ export type RecordsWriteOptions = {
dataFormat: string;

/**
* Signer of the message.
* The signer of the message, which is commonly the author, but can also be a delegate.
*/
signer?: Signer;

/**
* The delegated grant to sign on behalf of the logical author, which is the grantor (`grantedBy`) of the delegated grant.
* The delegated grant invoked to sign on behalf of the logical author, which is the grantor of the delegated grant.
*/
delegatedGrant?: DelegatedGrantMessage;

Expand Down Expand Up @@ -136,8 +136,9 @@ export type CreateFromOptions = {
messageTimestamp?: string;
datePublished?: string;


/**
* Signer of the message.
* The signer of the message, which is commonly the author, but can also be a delegate.
*/
signer?: Signer;

Expand Down Expand Up @@ -487,7 +488,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
}

/**
* Signs the RecordsWrite, commonly as author, but can also be a delegate.
* Signs the RecordsWrite, the signer is commonly the author, but can also be a delegate.
*/
public async sign(options: {
signer: Signer,
Expand Down
277 changes: 277 additions & 0 deletions src/protocols/permissions.ts
@@ -0,0 +1,277 @@
import type { ProtocolDefinition } from '../types/protocols-types.js';
import type { Signer } from '../types/signer.js';
import type { PermissionConditions, PermissionGrantModel, PermissionRequestModel, PermissionRevocationModel, PermissionScope, RecordsPermissionScope } from '../types/permissions-grant-descriptor.js';

import { Encoder } from '../utils/encoder.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { normalizeProtocolUrl, normalizeSchemaUrl } from '../utils/url.js';

/**
* Options for creating a permission request.
*/
export type PermissionRequestCreateOptions = {
/**
* The signer of the request.
*/
signer?: Signer;

dateRequested?: string;

// remaining properties are contained within the data payload of the record

description?: string;
delegated: boolean;
scope: PermissionScope;
conditions?: PermissionConditions;
};

/**
* Options for creating a permission grant.
*/
export type PermissionGrantCreateOptions = {
/**
* The signer of the grant.
*/
signer?: Signer;
grantedTo: string;
dateGranted?: string;

// remaining properties are contained within the data payload of the record

/**
* Expire time in UTC ISO-8601 format with microsecond precision.
*/
dateExpires: string;
requestId?: string;
description?: string;
delegated?: boolean;
scope: PermissionScope;
conditions?: PermissionConditions;
};

/**
* Options for creating a permission revocation.
*/
export type PermissionRevocationCreateOptions = {
/**
* The signer of the grant.
*/
signer?: Signer;
grantId: string;
dateRevoked?: string;

// remaining properties are contained within the data payload of the record

description?: string;
};

/**
* This is a first-class DWN protocol for managing permission grants of a given DWN.
*/
export class PermissionsProtocol {
/**
* The URI of the DWN Permissions protocol.
*/
public static readonly uri = 'https://tbd.website/dwn/permissions';

/**
* The protocol path of the `request` record.
*/
public static readonly requestPath = 'request';

/**
* The protocol path of the `grant` record.
*/
public static readonly grantPath = 'grant';

/**
* The protocol path of the `revocation` record.
*/
public static readonly revocationPath = 'grant/revocation';

/**
* The definition of the Permissions protocol.
*/
public static readonly definition: ProtocolDefinition = {
published : true,
protocol : PermissionsProtocol.uri,
types : {
request: {
dataFormats: ['application/json']
},
grant: {
dataFormats: ['application/json']
},
revocation: {
dataFormats: ['application/json']
}
},
structure: {
request: {
$size: {
max: 10000
},
$actions: [
{
who : 'anyone',
can : ['create']
}
]
},
grant: {
$size: {
max: 10000
},
$actions: [
{
who : 'recipient',
of : 'grant',
can : ['read', 'query']
}
],
revocation: {
$size: {
max: 10000
},
$actions: [
{
who : 'anyone',
can : ['read']
}
]
}
}
}
};

public static parseRequest(base64UrlEncodedRequest: string): PermissionRequestModel {
return Encoder.base64UrlToObject(base64UrlEncodedRequest);
}

/**
* Convenience method to create a permission request.
*/
public static async createRequest(options: PermissionRequestCreateOptions): Promise<{
recordsWrite: RecordsWrite,
permissionRequestModel: PermissionRequestModel,
permissionRequestBytes: Uint8Array
}> {
const scope = PermissionsProtocol.normalizePermissionScope(options.scope);

const permissionRequestModel: PermissionRequestModel = {
description : options.description,
delegated : options.delegated,
scope,
conditions : options.conditions,
};

const permissionRequestBytes = Encoder.objectToBytes(permissionRequestModel);
const recordsWrite = await RecordsWrite.create({
signer : options.signer,
messageTimestamp : options.dateRequested,
protocol : PermissionsProtocol.uri,
protocolPath : PermissionsProtocol.requestPath,
dataFormat : 'application/json',
data : permissionRequestBytes,
});

return {
recordsWrite,
permissionRequestModel,
permissionRequestBytes
};
}

/**
* Convenience method to create a permission grant.
*/
public static async createGrant(options: PermissionGrantCreateOptions): Promise<{
recordsWrite: RecordsWrite,
permissionGrantModel: PermissionGrantModel,
permissionGrantBytes: Uint8Array
}> {
const scope = PermissionsProtocol.normalizePermissionScope(options.scope);

const permissionGrantModel: PermissionGrantModel = {
dateExpires : options.dateExpires,
requestId : options.requestId,
description : options.description,
delegated : options.delegated,
scope,
conditions : options.conditions,
};

const permissionGrantBytes = Encoder.objectToBytes(permissionGrantModel);
const recordsWrite = await RecordsWrite.create({
signer : options.signer,
messageTimestamp : options.dateGranted,
recipient : options.grantedTo,
protocol : PermissionsProtocol.uri,
protocolPath : PermissionsProtocol.grantPath,
dataFormat : 'application/json',
data : permissionGrantBytes,
});

return {
recordsWrite,
permissionGrantModel,
permissionGrantBytes
};
}

/**
* Convenience method to create a permission revocation.
*/
public static async createRevocation(options: PermissionRevocationCreateOptions): Promise<{
recordsWrite: RecordsWrite,
permissionRevocationModel: PermissionRevocationModel,
permissionRevocationBytes: Uint8Array
}> {
const permissionRevocationModel: PermissionRevocationModel = {
description: options.description,
};

const permissionRevocationBytes = Encoder.objectToBytes(permissionRevocationModel);
const recordsWrite = await RecordsWrite.create({
signer : options.signer,
parentContextId : options.grantId,
protocol : PermissionsProtocol.uri,
protocolPath : PermissionsProtocol.revocationPath,
dataFormat : 'application/json',
data : permissionRevocationBytes,
});

return {
recordsWrite,
permissionRevocationModel,
permissionRevocationBytes
};
}

/**
* Normalizes the given permission scope if needed.
* @returns The normalized permission scope.
*/
private static normalizePermissionScope(permissionScope: PermissionScope): PermissionScope {
const scope = { ...permissionScope };

if (PermissionsProtocol.isRecordPermissionScope(scope)) {
// normalize protocol and schema URLs if they are present
if (scope.protocol !== undefined) {
scope.protocol = normalizeProtocolUrl(scope.protocol);
}
if (scope.schema !== undefined) {
scope.schema = normalizeSchemaUrl(scope.schema);
}
}

return scope;
}

/**
* Type guard to determine if the scope is a record permission scope.
*/
private static isRecordPermissionScope(scope: PermissionScope): scope is RecordsPermissionScope {
return scope.interface === 'Records';
}
};

0 comments on commit ee68791

Please sign in to comment.