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

RecordsPermissionScope Revocation must be scoped to a protocol #754

Merged
merged 14 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export enum DwnErrorCode {
PermissionsProtocolValidateScopeContextIdProhibitedProperties = 'PermissionsProtocolValidateScopeContextIdProhibitedProperties',
PermissionsProtocolValidateScopeProtocolMismatch = 'PermissionsProtocolValidateScopeProtocolMismatch',
PermissionsProtocolValidateScopeMissingProtocolTag = 'PermissionsProtocolValidateScopeMissingProtocolTag',
PermissionsProtocolValidateRevocationProtocolTagMismatch = 'PermissionsProtocolValidateRevocationProtocolTagMismatch',
PrivateKeySignerUnableToDeduceAlgorithm = 'PrivateKeySignerUnableToDeduceAlgorithm',
PrivateKeySignerUnableToDeduceKeyId = 'PrivateKeySignerUnableToDeduceKeyId',
PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve',
Expand Down
35 changes: 35 additions & 0 deletions src/handlers/records-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export class RecordsWriteHandler implements MethodHandler {
}

try {
// NOTE: We want to perform additional validation before storing the RecordsWrite.
// This is necessary for core DWN RecordsWrite that needs additional processing and allows us to fail before the storing and post processing.
//
// Example: Ensures that the protocol tag of a permission revocation RecordsWrite and the parent grant's scoped protocol match.
await this.preProcessingForCoreRecordsWrite(tenant, message);

// NOTE: We allow isLatestBaseState to be true ONLY if the incoming message comes with data, or if the incoming message is NOT an initial write
// This would allow an initial write to be written to the DB without data, but having it not queryable,
// because query implementation filters on `isLatestBaseState` being `true`
Expand Down Expand Up @@ -170,6 +176,35 @@ export class RecordsWriteHandler implements MethodHandler {
return messageReply;
};

/**
* Performs additional necessary validation before storing the RecordsWrite if it is a core DWN RecordsWrite that needs additional processing.
* For instance: a Permission revocation RecordsWrite.
*/
private async preProcessingForCoreRecordsWrite(tenant: string, recordsWriteMessage: RecordsWriteMessage): Promise<void> {

// we validate the protocol tag of the revocation message against the grant's scoped protocol
// to do this we will fetch the grant, and compare the the scoped protocol value to the protocol tag of the revocation message
if (recordsWriteMessage.descriptor.protocol === PermissionsProtocol.uri &&
recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.revocationPath) {

// get the parentId of the revocation message, which is the permissionGrantId
// fetch the grant in order to get the grant's protocol
const permissionGrantId = recordsWriteMessage.descriptor.parentId!;
const grant = await PermissionsProtocol.fetchGrant(tenant, this.messageStore, permissionGrantId);

// get the protocol values of the revocation message from the protocol tag and the protocol from the grant scope if they are defined
// compare the two values ensuring they must match
const revokeTagProtocol = recordsWriteMessage.descriptor.tags?.protocol;
const grantProtocol = 'protocol' in grant.scope ? grant.scope.protocol : undefined;
if (grantProtocol !== revokeTagProtocol) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateRevocationProtocolTagMismatch,
`Revocation protocol ${revokeTagProtocol} does not match grant protocol ${grantProtocol}`
);
}
}
}

private static validateSchemaForCoreRecordsWrite(recordsWriteMessage: RecordsWriteMessage, dataBytes: Uint8Array): void {
if (recordsWriteMessage.descriptor.protocol === PermissionsProtocol.uri) {
PermissionsProtocol.validateSchema(recordsWriteMessage, dataBytes);
Expand Down
25 changes: 23 additions & 2 deletions src/protocols/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export type PermissionRevocationCreateOptions = {
* The signer of the grant.
*/
signer?: Signer;
grantId: string;
/**
* The PermissionGrant this revocation is for.
*/
grant: PermissionGrant;
dateRevoked?: string;

// remaining properties are contained within the data payload of the record
Expand Down Expand Up @@ -181,6 +184,8 @@ export class PermissionsProtocol {
conditions : options.conditions,
};

// If the request is scoped to a protocol, the protocol tag must be included with the record.
// This is done in order to ensure a subset message query filtered to a protocol includes the permission requests associated with it.
let permissionTags = undefined;
if (this.isRecordPermissionScope(scope)) {
permissionTags = {
Expand Down Expand Up @@ -234,6 +239,8 @@ export class PermissionsProtocol {
conditions : options.conditions,
};

// If the grant is scoped to a protocol, the protocol tag must be included with the record.
// This is done in order to ensure a subset message query filtered to a protocol includes the permission grants associated with it.
let permissionTags = undefined;
if (this.isRecordPermissionScope(scope)) {
permissionTags = {
Expand Down Expand Up @@ -279,14 +286,28 @@ export class PermissionsProtocol {
description: options.description,
};

const grantId = options.grant.id;
const grantScopedProtocol = this.isRecordPermissionScope(options.grant.scope) ? options.grant.scope.protocol : undefined;

// if the grant was scoped to a protocol, the protocol tag must be included in the revocation
// This is done in order to ensure a subset message query filtered to a protocol includes the permission revocations associated with it.
//
// NOTE: the added tag is validated against the original grant when the revocation is processed by the DWN.
let permissionTags = undefined;
if (grantScopedProtocol !== undefined) {
const protocol = normalizeProtocolUrl(grantScopedProtocol);
permissionTags = { protocol };
}

const permissionRevocationBytes = Encoder.objectToBytes(permissionRevocationData);
const recordsWrite = await RecordsWrite.create({
signer : options.signer,
parentContextId : options.grantId, // NOTE: since the grant is the root record, its record ID is also the context ID
parentContextId : grantId, // NOTE: since the grant is the root record, its record ID is also the context ID
protocol : PermissionsProtocol.uri,
protocolPath : PermissionsProtocol.revocationPath,
dataFormat : 'application/json',
data : permissionRevocationBytes,
tags : permissionTags,
});

return {
Expand Down
5 changes: 3 additions & 2 deletions tests/features/author-delegated-grant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DataStream } from '../../src/utils/data-stream.js';
import { Dwn } from '../../src/dwn.js';
import { DwnErrorCode } from '../../src/core/dwn-error.js';
import { Jws } from '../../src/utils/jws.js';
import { PermissionGrant } from '../../src/protocols/permission-grant.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { TestDataGenerator } from '../utils/test-data-generator.js';
import { TestEventStream } from '../test-event-stream.js';
Expand Down Expand Up @@ -1189,8 +1190,8 @@ export function testAuthorDelegatedGrant(): void {

// 3. Alice revokes the grant
const permissionRevoke = await PermissionsProtocol.createRevocation({
signer : Jws.createSigner(alice),
grantId : deviceXGrant.recordsWrite.message.recordId
signer : Jws.createSigner(alice),
grant : await PermissionGrant.parse(deviceXGrant.dataEncodedMessage),
});
const revocationDataStream = DataStream.fromBytes(permissionRevoke.permissionRevocationBytes);
const permissionRevokeReply = await dwn.processMessage(alice.did, permissionRevoke.recordsWrite.message, { dataStream: revocationDataStream });
Expand Down
5 changes: 3 additions & 2 deletions tests/features/owner-delegated-grant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DataStream } from '../../src/utils/data-stream.js';
import { Dwn } from '../../src/dwn.js';
import { DwnErrorCode } from '../../src/core/dwn-error.js';
import { Jws } from '../../src/utils/jws.js';
import { PermissionGrant } from '../../src/protocols/permission-grant.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { TestDataGenerator } from '../utils/test-data-generator.js';
import { TestEventStream } from '../test-event-stream.js';
Expand Down Expand Up @@ -601,8 +602,8 @@ export function testOwnerDelegatedGrant(): void {

// 3. Alice revokes the grant
const permissionRevoke = await PermissionsProtocol.createRevocation({
signer : Jws.createSigner(alice),
grantId : appXGrant.recordsWrite.message.recordId
signer : Jws.createSigner(alice),
grant : await PermissionGrant.parse(appXGrant.dataEncodedMessage),
});
const revocationDataStream = DataStream.fromBytes(permissionRevoke.permissionRevocationBytes);
const permissionRevokeReply = await dwn.processMessage(
Expand Down
Loading