Skip to content

Commit

Permalink
RecordsPermissionScope Grants must be scoped to a protocol (#750)
Browse files Browse the repository at this point in the history
Modifies `RecordsPermissionScope` to require scoping to a specific
protocol. This also removes the ability to scope to a schema as you
would just scope to a protocolPath instead.

The motivation behind this was to make querying for all Messages tied to
a specific protocol for subset sync. Without this change getting grants,
and more importantly the grant revocations which have unrestricted
scopes would be challenging/not performant.

Since we are planning on getting rid of flat-spaced records anyway, this
is a step in the right direction.

- Remove `schema` from `RecordsPermissionScope`
- `RecordsPermissionScope` requires `protocol` to be set
- The `protocol` provided within the `RecordsPermissionScope` is also
added as a `protocol` tag to the `RecordsWriteMessage` descriptor.
- When validating the Scope of a Grant `RecordsWriteMessage`, ensure the
`protocol` tag on the record is the same as the scope `protocol` defined
in the `RecordsPermissionScope`.


`grant` case is covered by this PR.
  • Loading branch information
LiranCohen authored Jun 13, 2024
1 parent 1894c1a commit 661fad2
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 639 deletions.
5 changes: 3 additions & 2 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ export enum DwnErrorCode {
MessageGetInvalidCid = 'MessageGetInvalidCid',
ParseCidCodecNotSupported = 'ParseCidCodecNotSupported',
ParseCidMultihashNotSupported = 'ParseCidMultihashNotSupported',
PermissionsProtocolCreateGrantRecordsScopeMissingProtocol = 'PermissionsProtocolCreateGrantRecordsScopeMissingProtocol',
PermissionsProtocolValidateSchemaUnexpectedRecord = 'PermissionsProtocolValidateSchemaUnexpectedRecord',
PermissionsProtocolValidateScopeContextIdProhibitedProperties = 'PermissionsProtocolValidateScopeContextIdProhibitedProperties',
PermissionsProtocolValidateScopeSchemaProhibitedProperties = 'PermissionsProtocolValidateScopeSchemaProhibitedProperties',
PermissionsProtocolValidateScopeProtocolMismatch = 'PermissionsProtocolValidateScopeProtocolMismatch',
PermissionsProtocolValidateScopeMissingProtocolTag = 'PermissionsProtocolValidateScopeMissingProtocolTag',
PrivateKeySignerUnableToDeduceAlgorithm = 'PrivateKeySignerUnableToDeduceAlgorithm',
PrivateKeySignerUnableToDeduceKeyId = 'PrivateKeySignerUnableToDeduceKeyId',
PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve',
Expand Down Expand Up @@ -101,7 +103,6 @@ export enum DwnErrorCode {
RecordsGrantAuthorizationDeleteProtocolScopeMismatch = 'RecordsGrantAuthorizationDeleteProtocolScopeMismatch',
RecordsGrantAuthorizationQueryOrSubscribeProtocolScopeMismatch = 'RecordsGrantAuthorizationQueryOrSubscribeProtocolScopeMismatch',
RecordsGrantAuthorizationScopeContextIdMismatch = 'RecordsGrantAuthorizationScopeContextIdMismatch',
RecordsGrantAuthorizationScopeMissingProtocol = 'RecordsGrantAuthorizationScopeMissingProtocol',
RecordsGrantAuthorizationScopeNotRecords = `RecordsGrantAuthorizationScopeNotRecords`,
RecordsGrantAuthorizationScopeProtocolMismatch = 'RecordsGrantAuthorizationScopeProtocolMismatch',
RecordsGrantAuthorizationScopeProtocolPathMismatch = 'RecordsGrantAuthorizationScopeProtocolPathMismatch',
Expand Down
53 changes: 1 addition & 52 deletions src/core/records-grant-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,37 +138,12 @@ export class RecordsGrantAuthorization {
}

/**
* Verifies the given record against the scope of the given grant.
* Verifies a record against the scope of the given grant.
*/
private static verifyScope(
recordsWriteMessage: RecordsWriteMessage,
grantScope: RecordsPermissionScope,
): void {
if (RecordsGrantAuthorization.isUnrestrictedScope(grantScope)) {
// scope has no restrictions beyond interface and method. Message is authorized to access any record.
return;
} else if (recordsWriteMessage.descriptor.protocol !== undefined) {
// authorization of protocol records must have grants that explicitly include the protocol
RecordsGrantAuthorization.verifyProtocolRecordScope(recordsWriteMessage, grantScope);
} else {
RecordsGrantAuthorization.verifyFlatRecordScope(recordsWriteMessage, grantScope);
}
}

/**
* Verifies a protocol record against the scope of the given grant.
*/
private static verifyProtocolRecordScope(
recordsWriteMessage: RecordsWriteMessage,
grantScope: RecordsPermissionScope
): void {
// Protocol records must have grants specifying the protocol
if (grantScope.protocol === undefined) {
throw new DwnError(
DwnErrorCode.RecordsGrantAuthorizationScopeMissingProtocol,
'Grant for protocol record must specify protocol in its scope'
);
}

// The record's protocol must match the protocol specified in the record
if (grantScope.protocol !== recordsWriteMessage.descriptor.protocol) {
Expand Down Expand Up @@ -197,23 +172,6 @@ export class RecordsGrantAuthorization {
}
}

/**
* Verifies a non-protocol record against the scope of the given grant.
*/
private static verifyFlatRecordScope(
recordsWriteMessage: RecordsWriteMessage,
grantScope: RecordsPermissionScope
): void {
if (grantScope.schema !== undefined) {
if (grantScope.schema !== recordsWriteMessage.descriptor.schema) {
throw new DwnError(
DwnErrorCode.RecordsGrantAuthorizationScopeSchema,
`Record does not have schema in permission grant scope with schema '${grantScope.schema}'`
);
}
}
}

/**
* Verifies grant `conditions`.
* Currently the only condition is `published` which only applies to RecordsWrites
Expand All @@ -236,13 +194,4 @@ export class RecordsGrantAuthorization {
);
}
}

/**
* Checks if scope has no restrictions beyond interface and method.
* Grant-holder is authorized to access any record.
*/
private static isUnrestrictedScope(grantScope: RecordsPermissionScope): boolean {
return grantScope.protocol === undefined &&
grantScope.schema === undefined;
}
}
70 changes: 42 additions & 28 deletions src/protocols/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Time } from '../utils/time.js';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { normalizeProtocolUrl, normalizeSchemaUrl } from '../utils/url.js';
import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../utils/url.js';

/**
* Options for creating a permission request.
Expand Down Expand Up @@ -199,6 +199,14 @@ export class PermissionsProtocol {
permissionGrantBytes: Uint8Array,
dataEncodedMessage: DataEncodedRecordsWriteMessage,
}> {

if (this.isRecordPermissionScope(options.scope) && options.scope.protocol === undefined) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolCreateGrantRecordsScopeMissingProtocol,
'Permission grants for Records must have a scope with a `protocol` property'
);
}

const scope = PermissionsProtocol.normalizePermissionScope(options.scope);

const permissionGrantData: PermissionGrantData = {
Expand All @@ -210,6 +218,13 @@ export class PermissionsProtocol {
conditions : options.conditions,
};

let permissionTags = undefined;
if (this.isRecordPermissionScope(scope)) {
permissionTags = {
protocol: scope.protocol
};
}

const permissionGrantBytes = Encoder.objectToBytes(permissionGrantData);
const recordsWrite = await RecordsWrite.create({
signer : options.signer,
Expand All @@ -220,6 +235,7 @@ export class PermissionsProtocol {
protocolPath : PermissionsProtocol.grantPath,
dataFormat : 'application/json',
data : permissionGrantBytes,
tags : permissionTags,
});

const dataEncodedMessage: DataEncodedRecordsWriteMessage = {
Expand Down Expand Up @@ -277,12 +293,12 @@ export class PermissionsProtocol {

// more nuanced validation that are annoying/difficult to do using JSON schema
const permissionGrantData = dataObject as PermissionGrantData;
PermissionsProtocol.validateScope(permissionGrantData.scope);
PermissionsProtocol.validateScope(permissionGrantData.scope, recordsWriteMessage);
Time.validateTimestamp(permissionGrantData.dateExpires);
} else if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.revocationPath) {
validateJsonSchema('PermissionRevocationData', dataObject);
} else {
// defensive programming, should be unreachable externally
// defensive programming, should not be unreachable externally
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateSchemaUnexpectedRecord,
`Unexpected permission record: ${recordsWriteMessage.descriptor.protocolPath}`
Expand Down Expand Up @@ -336,13 +352,7 @@ export class PermissionsProtocol {
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);
}
scope.protocol = normalizeProtocolUrl(scope.protocol);
}

return scope;
Expand All @@ -355,34 +365,38 @@ export class PermissionsProtocol {
return scope.interface === 'Records';
}


/**
* Validates scope.
*/
private static validateScope(scope: PermissionScope): void {
private static validateScope(scope: PermissionScope, grantRecord: RecordsWriteMessage): void {
if (!this.isRecordPermissionScope(scope)) {
return;
}

// else we are dealing with a RecordsPermissionScope

// `schema` scopes may not have protocol-related fields
if (scope.schema !== undefined) {
if (scope.protocol !== undefined || scope.contextId !== undefined || scope.protocolPath) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateScopeSchemaProhibitedProperties,
'Permission grants that have `schema` present cannot also have protocol-related properties present'
);
}
if (grantRecord.descriptor.tags === undefined || grantRecord.descriptor.tags.protocol === undefined) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateScopeMissingProtocolTag,
'Permission grants must have a `tags` property that contains a protocol tag'
);
}
const taggedProtocol = grantRecord.descriptor.tags.protocol as string;
validateProtocolUrlNormalized(taggedProtocol);

if (scope.protocol !== undefined) {
// `contextId` and `protocolPath` are mutually exclusive
if (scope.contextId !== undefined && scope.protocolPath !== undefined) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateScopeContextIdProhibitedProperties,
'Permission grants cannot have both `contextId` and `protocolPath` present'
);
}
if (scope.protocol !== taggedProtocol) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateScopeProtocolMismatch,
`Permission grants must have a scope with a protocol that matches the tagged protocol: ${taggedProtocol}`
);
}

// `contextId` and `protocolPath` are mutually exclusive
if (scope.contextId !== undefined && scope.protocolPath !== undefined) {
throw new DwnError(
DwnErrorCode.PermissionsProtocolValidateScopeContextIdProhibitedProperties,
'Permission grants cannot have both `contextId` and `protocolPath` present'
);
}
}
};
25 changes: 17 additions & 8 deletions src/types/permission-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,34 @@ export type PermissionRevocationData = {
/**
* The data model for a permission scope.
*/
export type PermissionScope = {
interface: DwnInterfaceName;
method: DwnMethodName;
} | RecordsPermissionScope;
export type PermissionScope = ProtocolPermissionScope | MessagesPermissionScope | EventsPermissionScope | RecordsPermissionScope;

export type ProtocolPermissionScope = {
interface: DwnInterfaceName.Protocols;
method: DwnMethodName.Configure | DwnMethodName.Query;
};

export type MessagesPermissionScope = {
interface: DwnInterfaceName.Messages;
method: DwnMethodName.Get;
};

export type EventsPermissionScope = {
interface: DwnInterfaceName.Events;
method: DwnMethodName.Get | DwnMethodName.Query | DwnMethodName.Subscribe;
};

/**
* The data model for a permission scope that is specific to the Records interface.
*/
export type RecordsPermissionScope = {
interface: DwnInterfaceName.Records;
method: DwnMethodName.Read | DwnMethodName.Write | DwnMethodName.Query | DwnMethodName.Subscribe | DwnMethodName.Delete;
/** May only be present when `schema` is undefined */
protocol?: string;
protocol: string;
/** May only be present when `protocol` is defined and `protocolPath` is undefined */
contextId?: string;
/** May only be present when `protocol` is defined and `contextId` is undefined */
protocolPath?: string;
/** May only be present when `protocol` is undefined */
schema?: string;
};

export enum PermissionConditionPublication {
Expand Down
Loading

0 comments on commit 661fad2

Please sign in to comment.