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

Allowed prune after a regular delete #798

Merged
merged 1 commit into from
Aug 19, 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
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