Skip to content

Commit

Permalink
Allowed prune after a regular delete (#798)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Aug 19, 2024
1 parent a233e8a commit 8d574ed
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 23 deletions.
12 changes: 6 additions & 6 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,12 @@ export class ProtocolAuthorization {

/**
* Performs protocol-based authorization against the incoming `RecordsDelete` message.
* @param newestRecordsWrite The latest `RecordsWrite` associated with the recordId being deleted.
* @param recordsWrite A `RecordsWrite` of the record being deleted.
*/
public static async authorizeDelete(
tenant: string,
incomingMessage: RecordsDelete,
newestRecordsWrite: RecordsWrite,
recordsWrite: RecordsWrite,
messageStore: MessageStore,
): Promise<void> {

Expand All @@ -228,22 +228,22 @@ export class ProtocolAuthorization {
// fetch the protocol definition
const protocolDefinition = await ProtocolAuthorization.fetchProtocolDefinition(
tenant,
newestRecordsWrite.message.descriptor.protocol!,
recordsWrite.message.descriptor.protocol!,
messageStore,
);

// get the rule set for the inbound message
const ruleSet = ProtocolAuthorization.getRuleSet(
newestRecordsWrite.message.descriptor.protocolPath!,
recordsWrite.message.descriptor.protocolPath!,
protocolDefinition,
);

// If the incoming message has `protocolRole` in the descriptor, validate the invoked role
await ProtocolAuthorization.verifyInvokedRole(
tenant,
incomingMessage,
newestRecordsWrite.message.descriptor.protocol!,
newestRecordsWrite.message.contextId!,
recordsWrite.message.descriptor.protocol!,
recordsWrite.message.contextId!,
protocolDefinition,
messageStore,
);
Expand Down
27 changes: 16 additions & 11 deletions src/handlers/records-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import type { DidResolver } from '@web5/dids';
import type { GenericMessageReply } from '../types/message-types.js';
import type { MessageStore } from '../types//message-store.js';
import type { MethodHandler } from '../types/method-handler.js';
import type { RecordsDeleteMessage } from '../types/records-types.js';
import type { ResumableTaskManager } from '../core/resumable-task-manager.js';
import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';

import { authenticate } from '../core/auth.js';
import { DwnInterfaceName } from '../enums/dwn-interface-method.js';
import { Message } from '../core/message.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
import { Records } from '../utils/records.js';
import { RecordsDelete } from '../interfaces/records-delete.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { ResumableTaskName } from '../core/resumable-task-manager.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';

export class RecordsDeleteHandler implements MethodHandler {

Expand Down Expand Up @@ -51,15 +52,14 @@ export class RecordsDeleteHandler implements MethodHandler {
// find which message is the newest, and if the incoming message is the newest
const newestExistingMessage = await Message.getNewestMessage(existingMessages);

// return Not Found if record does not exist or is already deleted
if (newestExistingMessage === undefined || newestExistingMessage.descriptor.method === DwnMethodName.Delete) {
if (!Records.canPerformDeleteAgainstRecord(message, newestExistingMessage)) {
return {
status: { code: 404, detail: 'Not Found' }
};
}

// if the incoming message is not the newest, return Conflict
const incomingDeleteIsNewest = await Message.isNewer(message, newestExistingMessage);
const incomingDeleteIsNewest = await Message.isNewer(message, newestExistingMessage!);
if (!incomingDeleteIsNewest) {
return {
status: { code: 409, detail: 'Conflict' }
Expand All @@ -68,10 +68,15 @@ export class RecordsDeleteHandler implements MethodHandler {

// authorization
try {
// NOTE: We need a RecordsWrite (doesn't have to be initial) to access the immutable properties for delete processing,
// but if the latest record state is a RecordsDelete (ie. when we are pruning a non-prune delete),
// we'd need to use the initial write because RecordsDelete does not contain the immutable properties needed for processing.
const initialWrite = await RecordsWrite.fetchInitialRecordsWrite(this.messageStore, tenant, message.descriptor.recordId);

await RecordsDeleteHandler.authorizeRecordsDelete(
tenant,
recordsDelete,
await RecordsWrite.parse(newestExistingMessage as RecordsWriteMessage),
initialWrite!,
this.messageStore
);
} catch (e) {
Expand All @@ -92,23 +97,23 @@ export class RecordsDeleteHandler implements MethodHandler {
/**
* Authorizes a RecordsDelete message.
*
* @param newestRecordsWrite Newest RecordsWrite of the record to be deleted.
* @param recordsWrite A RecordsWrite of the record to be deleted.
*/
private static async authorizeRecordsDelete(
tenant: string,
recordsDelete: RecordsDelete,
newestRecordsWrite: RecordsWrite,
recordsWrite: RecordsWrite,
messageStore: MessageStore
): Promise<void> {

if (Message.isSignedByAuthorDelegate(recordsDelete.message)) {
await recordsDelete.authorizeDelegate(newestRecordsWrite.message, messageStore);
await recordsDelete.authorizeDelegate(recordsWrite.message, messageStore);
}

if (recordsDelete.author === tenant) {
return;
} else if (newestRecordsWrite.message.descriptor.protocol !== undefined) {
await ProtocolAuthorization.authorizeDelete(tenant, recordsDelete, newestRecordsWrite, messageStore);
} else if (recordsWrite.message.descriptor.protocol !== undefined) {
await ProtocolAuthorization.authorizeDelete(tenant, recordsDelete, recordsWrite, messageStore);
} else {
throw new DwnError(
DwnErrorCode.RecordsDeleteAuthorizationFailed,
Expand Down
3 changes: 1 addition & 2 deletions src/store/storage-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ export class StorageController {
// find which message is the newest, and if the incoming message is the newest
const newestExistingMessage = await Message.getNewestMessage(existingMessages);

// if no messages found for the record, nothing to do
if (newestExistingMessage === undefined || newestExistingMessage.descriptor.method === DwnMethodName.Delete) {
if (!Records.canPerformDeleteAgainstRecord(message, newestExistingMessage)) {
return;
}

Expand Down
28 changes: 25 additions & 3 deletions src/utils/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,26 +487,48 @@ export class Records {
/**
* Determines if signature payload contains a protocolRole and should be authorized as such.
*/
static shouldProtocolAuthorize(signaturePayload: GenericSignaturePayload): boolean {
public static shouldProtocolAuthorize(signaturePayload: GenericSignaturePayload): boolean {
return signaturePayload.protocolRole !== undefined;
}

/**
* Checks if the filter supports returning published records.
*/
static filterIncludesPublishedRecords(filter: RecordsFilter): boolean {
public static filterIncludesPublishedRecords(filter: RecordsFilter): boolean {
// NOTE: published records should still be returned when `published` and `datePublished` range are both undefined.
return filter.datePublished !== undefined || filter.published !== false;
}

/**
* Checks if the filter supports returning unpublished records.
*/
static filterIncludesUnpublishedRecords(filter: RecordsFilter): boolean {
public static filterIncludesUnpublishedRecords(filter: RecordsFilter): boolean {
// When `published` and `datePublished` range are both undefined, unpublished records can be returned.
if (filter.datePublished === undefined && filter.published === undefined) {
return true;
}
return filter.published === false;
}

/**
* Checks if the given RecordsDelete message can be performed against a record with the given newest existing state.
*/
public static canPerformDeleteAgainstRecord(deleteToBePerformed: RecordsDeleteMessage, newestExistingMessage: GenericMessage | undefined): boolean {
if (newestExistingMessage === undefined) {
return false;
}

// can't perform delete if:
// attempting to delete on an already deleted record; or
// attempting to prune on an already pruned record;
if (newestExistingMessage.descriptor.method === DwnMethodName.Delete) {
if (deleteToBePerformed.descriptor.prune !== true) {
return false;
} else if ((newestExistingMessage as RecordsDeleteMessage).descriptor.prune === true) {
return false;
}
}

return true;
}
}
Loading

0 comments on commit 8d574ed

Please sign in to comment.