From c752775a1c67f9d4e3ca2cb51f9efda0314f85cb Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 30 May 2023 08:18:01 -0500 Subject: [PATCH] wip: vault sync filter --- .../Bootstrap/ServerContainerConfigLoader.ts | 15 +- .../syncing-server/src/Bootstrap/Types.ts | 3 +- .../src/Domain/Item/ItemConflict.ts | 2 +- .../src/Domain/Item/ItemService.ts | 6 +- .../src/Domain/Item/SaveRule/ContentFilter.ts | 2 +- .../Domain/Item/SaveRule/ContentTypeFilter.ts | 2 +- .../Domain/Item/SaveRule/OwnershipFilter.ts | 162 ++-------- .../Item/SaveRule/TimeDifferenceFilter.ts | 2 +- .../src/Domain/Item/SaveRule/UuidFilter.ts | 2 +- .../src/Domain/Item/SaveRule/VaultFilter.ts | 295 ++++++++++++++++++ .../Item/SaveRule/VaultSaveOperation.ts | 44 +++ ...ionFilter.ts => VaultSnjsVersionFilter.ts} | 8 +- .../Item/SyncResponse/SyncResponse20161215.ts | 3 +- .../SyncResponseFactory20161215.ts | 2 +- .../src/Projection/ItemConflictProjection.ts | 3 +- .../syncing-server/src/Tmp/ConflictType.ts | 14 + 16 files changed, 401 insertions(+), 164 deletions(-) create mode 100644 packages/syncing-server/src/Domain/Item/SaveRule/VaultFilter.ts create mode 100644 packages/syncing-server/src/Domain/Item/SaveRule/VaultSaveOperation.ts rename packages/syncing-server/src/Domain/Item/SaveRule/{SnjsVersionFilter.ts => VaultSnjsVersionFilter.ts} (79%) create mode 100644 packages/syncing-server/src/Tmp/ConflictType.ts diff --git a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts index 80e044339..a6815dee4 100644 --- a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts +++ b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts @@ -20,6 +20,7 @@ import { SyncItems } from '../Domain/UseCase/SyncItems' import { ItemConflictProjector } from '../Projection/ItemConflictProjector' import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface' import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator' +import { VaultFilter } from '../Domain/Item/SaveRule/VaultFilter' import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter' import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter' import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface' @@ -46,7 +47,7 @@ import { VaultInviteFactory } from '../Domain/VaultInvite/Factory/VaultInviteFac import { ContactServiceInterface } from '../Domain/Contact/Service/ContactServiceInterface' import { ContactService } from '../Domain/Contact/Service/ContactService' import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface' -import { SnjsVersionFilter } from '../Domain/Item/SaveRule/SnjsVersionFilter' +import { VaultSnjsVersionFilter } from '../Domain/Item/SaveRule/VaultSnjsVersionFilter' import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken' export class ServerContainerConfigLoader extends CommonContainerConfigLoader { @@ -226,11 +227,12 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { return new ContactFactory(context.container.get(TYPES.Timer)) }) + container.bind(TYPES.OwnershipFilter).toDynamicValue(() => new OwnershipFilter()) container - .bind(TYPES.OwnershipFilter) + .bind(TYPES.VaultFilter) .toDynamicValue( (context: interfaces.Context) => - new OwnershipFilter(context.container.get(TYPES.VaultService), context.container.get(TYPES.VaultUserService)), + new VaultFilter(context.container.get(TYPES.VaultService), context.container.get(TYPES.VaultUserService)), ) container .bind(TYPES.TimeDifferenceFilter) @@ -238,14 +240,17 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { container.bind(TYPES.UuidFilter).toDynamicValue(() => new UuidFilter()) container.bind(TYPES.ContentTypeFilter).toDynamicValue(() => new ContentTypeFilter()) container.bind(TYPES.ContentFilter).toDynamicValue(() => new ContentFilter()) - container.bind(TYPES.SnjsVersionFilter).toDynamicValue(() => new SnjsVersionFilter()) + container + .bind(TYPES.VaultSnjsVersionFilter) + .toDynamicValue(() => new VaultSnjsVersionFilter()) container .bind(TYPES.ItemSaveValidator) .toDynamicValue((context: interfaces.Context) => { return new ItemSaveValidator([ - context.container.get(TYPES.SnjsVersionFilter), context.container.get(TYPES.OwnershipFilter), + context.container.get(TYPES.VaultFilter), + context.container.get(TYPES.VaultSnjsVersionFilter), context.container.get(TYPES.TimeDifferenceFilter), context.container.get(TYPES.UuidFilter), context.container.get(TYPES.ContentTypeFilter), diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 85b0a6b19..62f982ddd 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -97,7 +97,8 @@ const TYPES = { ItemBackupService: Symbol.for('ItemBackupService'), ItemSaveValidator: Symbol.for('ItemSaveValidator'), OwnershipFilter: Symbol.for('OwnershipFilter'), - SnjsVersionFilter: Symbol.for('SnjsVersionFilter'), + VaultFilter: Symbol.for('VaultFilter'), + VaultSnjsVersionFilter: Symbol.for('VaultSnjsVersionFilter'), TimeDifferenceFilter: Symbol.for('TimeDifferenceFilter'), UuidFilter: Symbol.for('UuidFilter'), ContentTypeFilter: Symbol.for('ContentTypeFilter'), diff --git a/packages/syncing-server/src/Domain/Item/ItemConflict.ts b/packages/syncing-server/src/Domain/Item/ItemConflict.ts index f2b32c62a..daa629144 100644 --- a/packages/syncing-server/src/Domain/Item/ItemConflict.ts +++ b/packages/syncing-server/src/Domain/Item/ItemConflict.ts @@ -1,4 +1,4 @@ -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../Tmp/ConflictType' import { Item } from './Item' import { ItemHash } from './ItemHash' diff --git a/packages/syncing-server/src/Domain/Item/ItemService.ts b/packages/syncing-server/src/Domain/Item/ItemService.ts index 9791b2965..282023876 100644 --- a/packages/syncing-server/src/Domain/Item/ItemService.ts +++ b/packages/syncing-server/src/Domain/Item/ItemService.ts @@ -16,11 +16,11 @@ import { ItemServiceInterface } from './ItemServiceInterface' import { SaveItemsDTO } from './SaveItemsDTO' import { SaveItemsResult } from './SaveItemsResult' import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface' -import { ConflictType } from '@standardnotes/responses' import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface' import { ProjectorInterface } from '../../Projection/ProjectorInterface' import { ItemProjection } from '../../Projection/ItemProjection' import { VaultUserRepositoryInterface } from '../VaultUser/Repository/VaultUserRepositoryInterface' +import { ConflictType } from '../../Tmp/ConflictType' export class ItemService implements ItemServiceInterface { private readonly DEFAULT_ITEMS_LIMIT = 150 @@ -100,16 +100,18 @@ export class ItemService implements ItemServiceInterface { const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds() for (const itemHash of dto.itemHashes) { + const existingItem = await this.itemRepository.findByUuid(itemHash.uuid) + if (dto.readOnlyAccess) { conflicts.push({ unsavedItem: itemHash, + serverItem: existingItem ?? undefined, type: ConflictType.ReadOnlyError, }) continue } - const existingItem = await this.itemRepository.findByUuid(itemHash.uuid) const processingResult = await this.itemSaveValidator.validate({ userUuid: dto.userUuid, apiVersion: dto.apiVersion, diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.ts index 07a8dd841..d4acc2dc7 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.ts @@ -1,7 +1,7 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../../Tmp/ConflictType' export class ContentFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts index 7620e27e1..4a318fe58 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts @@ -3,7 +3,7 @@ import { ContentType } from '@standardnotes/common' import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../../Tmp/ConflictType' export class ContentTypeFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts index 9ca70dd15..5bc9dc05f 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts @@ -1,152 +1,30 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' -import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface' -import { ItemHash } from '../ItemHash' -import { VaultUserPermission } from '../../VaultUser/Model/VaultUserPermission' -import { VaultServiceInterface } from '../../Vault/Service/VaultServiceInterface' -import { ContentType } from '@standardnotes/common' +import { ConflictType } from '../../../Tmp/ConflictType' export class OwnershipFilter implements ItemSaveRuleInterface { - constructor(private vaultService: VaultServiceInterface, private vaultUserService: VaultUserServiceInterface) {} - async check(dto: ItemSaveValidationDTO): Promise { - const successValue = { + const deferToVaultFilter = dto.existingItem?.vaultUuid || dto.itemHash.vault_uuid + if (deferToVaultFilter) { + return { + passed: true, + } + } + + const itemBelongsToADifferentUser = dto.existingItem !== null && dto.existingItem.userUuid !== dto.userUuid + if (itemBelongsToADifferentUser) { + return { + passed: false, + conflict: { + unsavedItem: dto.itemHash, + type: ConflictType.UuidConflict, + }, + } + } + + return { passed: true, } - - const vaultReadonlyFail = { - passed: false, - conflict: { - unsavedItem: dto.itemHash, - type: ConflictType.ReadOnlyError, - }, - } - - const ownershipFail = { - passed: false, - conflict: { - unsavedItem: dto.itemHash, - type: ConflictType.UuidConflict, - }, - } - - const contentFail = { - passed: false, - conflict: { - unsavedItem: dto.itemHash, - type: ConflictType.ContentError, - }, - } - - const vaultUuidInvolved = dto.existingItem?.vaultUuid || dto.itemHash.vault_uuid - if (vaultUuidInvolved) { - const result = await this.authorizationToSaveVaultInvolvedItem(dto) - if (result === 'ownership-fail') { - return ownershipFail - } else if (result === 'readonly-fail') { - return vaultReadonlyFail - } else if (result === 'content-fail') { - return contentFail - } else if (result === 'success') { - return successValue - } else { - throw new Error(`Unexpected vault authorization result: ${result}`) - } - } - - const itemBelongsToADifferentUser = - dto.existingItem != null && dto.existingItem.userUuid && dto.existingItem.userUuid !== dto.userUuid - if (itemBelongsToADifferentUser) { - return ownershipFail - } - - return successValue - } - - private async authorizationToSaveVaultInvolvedItem( - dto: ItemSaveValidationDTO, - ): Promise<'ownership-fail' | 'readonly-fail' | 'content-fail' | 'success'> { - const existingItemVaultUuid = dto.existingItem?.vaultUuid - const targetItemVaultUuid = dto.itemHash.vault_uuid - - const vaultUuidInvolved = existingItemVaultUuid || targetItemVaultUuid - - const isMovingVaults = existingItemVaultUuid && targetItemVaultUuid && existingItemVaultUuid !== targetItemVaultUuid - if (isMovingVaults) { - const existingVaultAuthorization = await this.vaultAuthorizationForItem(dto.userUuid, existingItemVaultUuid) - const targetVaultAuthorization = await this.vaultAuthorizationForItem(dto.userUuid, targetItemVaultUuid) - if (!existingVaultAuthorization || !targetVaultAuthorization) { - return 'ownership-fail' - } - - if (existingVaultAuthorization === 'read' || targetVaultAuthorization === 'read') { - return 'readonly-fail' - } - } - - const vaultAuthorization = await this.vaultAuthorizationForItem(dto.userUuid, vaultUuidInvolved as string) - if (!vaultAuthorization) { - return 'ownership-fail' - } - - if (vaultAuthorization === 'read') { - return 'readonly-fail' - } - - const isItemBeingRemovedFromVault = - dto.existingItem != null && dto.existingItem.vaultUuid != null && dto.itemHash.vault_uuid == null - - const isItemBeingDeleted = dto.itemHash.deleted === true - - if (isItemBeingRemovedFromVault || isItemBeingDeleted) { - const doesHavePermissionToRemoveItemFromVault = - vaultAuthorization === 'admin' || dto.existingItem?.createdByUuid === dto.userUuid - - return doesHavePermissionToRemoveItemFromVault ? 'success' : 'readonly-fail' - } - - if (dto.itemHash.content_type === ContentType.VaultItemsKey && vaultAuthorization !== 'admin') { - return 'readonly-fail' - } - - const usingValidKey = await this.vaultItemIsBeingSavedWithValidItemsKey(dto.itemHash) - if (!usingValidKey) { - return 'content-fail' - } - - return 'success' - } - - private async vaultItemIsBeingSavedWithValidItemsKey(itemHash: ItemHash): Promise { - const isItemNotEncryptedByItemsKey = itemHash.content_type === ContentType.VaultItemsKey - if (isItemNotEncryptedByItemsKey) { - return true - } - - const vault = await this.vaultService.getVault({ vaultUuid: itemHash.vault_uuid as string }) - - if (!vault) { - return false - } - - return itemHash.items_key_id === vault.specifiedItemsKeyUuid - } - - private async vaultAuthorizationForItem( - userUuid: string, - vaultUuid: string, - ): Promise { - const vaultUser = await this.vaultUserService.getUserForVault({ - userUuid, - vaultUuid: vaultUuid, - }) - - if (vaultUser) { - return vaultUser.permissions - } - - return undefined } } diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts index 3e039d790..7baff8f04 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts @@ -5,7 +5,7 @@ import { ItemHash } from '../ItemHash' import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../../Tmp/ConflictType' export class TimeDifferenceFilter implements ItemSaveRuleInterface { constructor(private timer: TimerInterface) {} diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts index a42c2e84c..49a173e37 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts @@ -2,7 +2,7 @@ import { validate } from 'uuid' import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../../Tmp/ConflictType' export class UuidFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/VaultFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/VaultFilter.ts new file mode 100644 index 000000000..86ca6a133 --- /dev/null +++ b/packages/syncing-server/src/Domain/Item/SaveRule/VaultFilter.ts @@ -0,0 +1,295 @@ +import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' +import { ItemSaveRuleResult } from './ItemSaveRuleResult' +import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' +import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface' +import { ItemHash } from '../ItemHash' +import { VaultUserPermission } from '../../VaultUser/Model/VaultUserPermission' +import { VaultServiceInterface } from '../../Vault/Service/VaultServiceInterface' +import { ContentType } from '@standardnotes/common' +import { ConflictType } from '../../../Tmp/ConflictType' +import { + VaultSaveOperation, + AddToVaultSaveOperation, + RemoveFromVaultSaveOperation, + MoveToOtherVaultSaveOperation, + SaveToVaultSaveOperation, + CreateToVaultSaveOperation, +} from './VaultSaveOperation' + +export class VaultFilter implements ItemSaveRuleInterface { + constructor(private vaultService: VaultServiceInterface, private vaultUserService: VaultUserServiceInterface) {} + + async check(dto: ItemSaveValidationDTO): Promise { + if (!dto.existingItem?.vaultUuid && !dto.itemHash.vault_uuid) { + return { + passed: true, + } + } + + const operation = this.getSaveOperationType(dto) + return this.getResultForOperation(operation) + } + + private async getResultForOperation(operation: VaultSaveOperation): Promise { + if (operation.type === 'add-to-vault') { + return this.handleAddToVaultOperation(operation) + } else if (operation.type === 'remove-from-vault') { + return this.handleRemoveFromVaultOperation(operation) + } else if (operation.type === 'move-to-other-vault') { + return this.handleMoveToOtherVaultOperation(operation) + } else if (operation.type === 'save-to-vault') { + return this.handleSaveToVaultOperation(operation) + } else if (operation.type === 'create-to-vault') { + return this.handleCreateToVaultOperation(operation) + } + + throw new Error(`Unsupported vault operation: ${operation}`) + } + + private getSaveOperationType(dto: ItemSaveValidationDTO): VaultSaveOperation { + const existingItemVaultUuid = dto.existingItem?.vaultUuid + const targetItemVaultUuid = dto.itemHash.vault_uuid + + const common = { + incomingItem: dto.itemHash, + userUuid: dto.userUuid, + } + + if ( + dto.existingItem && + existingItemVaultUuid && + targetItemVaultUuid && + existingItemVaultUuid !== targetItemVaultUuid + ) { + return { + type: 'move-to-other-vault', + sourceVaultUuid: existingItemVaultUuid, + targetVaultUuid: targetItemVaultUuid, + existingItem: dto.existingItem, + ...common, + } + } + + if (dto.existingItem && existingItemVaultUuid && !targetItemVaultUuid) { + return { + type: 'remove-from-vault', + vaultUuid: existingItemVaultUuid, + existingItem: dto.existingItem, + ...common, + } + } + + if (dto.existingItem && !existingItemVaultUuid && targetItemVaultUuid) { + return { + type: 'add-to-vault', + vaultUuid: targetItemVaultUuid, + existingItem: dto.existingItem, + ...common, + } + } + + if (dto.existingItem && existingItemVaultUuid && existingItemVaultUuid === targetItemVaultUuid) { + return { + type: 'save-to-vault', + vaultUuid: existingItemVaultUuid, + existingItem: dto.existingItem, + ...common, + } + } + + if (!dto.existingItem && targetItemVaultUuid) { + return { + type: 'create-to-vault', + vaultUuid: targetItemVaultUuid, + ...common, + } + } + + throw new Error('Invalid save operation') + } + + private isAuthorizedToSaveContentType(contentType: ContentType, permissions: VaultUserPermission): boolean { + if (contentType === ContentType.VaultItemsKey) { + return permissions === 'admin' + } + + return true + } + + private buildFailResult(operation: VaultSaveOperation, type: ConflictType) { + return { + passed: false, + conflict: { + unsavedItem: operation.incomingItem, + type, + }, + } + } + + private buildSuccessValue(): ItemSaveRuleResult { + return { + passed: true, + } + } + + private async handleAddToVaultOperation(operation: AddToVaultSaveOperation): Promise { + const vaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.vaultUuid) + if (!vaultPermissions) { + return this.buildFailResult(operation, ConflictType.VaultNotMemberError) + } + + if (operation.existingItem.deleted || operation.incomingItem.deleted) { + return this.buildFailResult(operation, ConflictType.VaultInvalidState) + } + + if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, vaultPermissions)) { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if (vaultPermissions === 'read') { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if (operation.existingItem.userUuid !== operation.userUuid) { + return this.buildFailResult(operation, ConflictType.UuidConflict) + } + + const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem) + if (!usesValidKey) { + return this.buildFailResult(operation, ConflictType.VaultInvalidItemsKey) + } + + return this.buildSuccessValue() + } + + private async handleRemoveFromVaultOperation(operation: RemoveFromVaultSaveOperation): Promise { + const vaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.vaultUuid) + if (!vaultPermissions) { + return this.buildFailResult(operation, ConflictType.VaultNotMemberError) + } + + if (operation.existingItem.deleted || operation.incomingItem.deleted) { + return this.buildFailResult(operation, ConflictType.VaultInvalidState) + } + + if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, vaultPermissions)) { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if (vaultPermissions === 'read') { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem) + if (!usesValidKey) { + return this.buildFailResult(operation, ConflictType.VaultInvalidItemsKey) + } + + return this.buildSuccessValue() + } + + private async handleMoveToOtherVaultOperation(operation: MoveToOtherVaultSaveOperation): Promise { + const sourceVaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.sourceVaultUuid) + const targetVaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.targetVaultUuid) + + if (!sourceVaultPermissions || !targetVaultPermissions) { + return this.buildFailResult(operation, ConflictType.VaultNotMemberError) + } + + if (operation.existingItem.deleted || operation.incomingItem.deleted) { + return this.buildFailResult(operation, ConflictType.VaultInvalidState) + } + + if (sourceVaultPermissions === 'read' || targetVaultPermissions === 'read') { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if ( + !this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, sourceVaultPermissions) || + !this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, targetVaultPermissions) + ) { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem) + if (!usesValidKey) { + return this.buildFailResult(operation, ConflictType.VaultInvalidItemsKey) + } + + return this.buildSuccessValue() + } + + private async handleSaveToVaultOperation(operation: SaveToVaultSaveOperation): Promise { + const vaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.vaultUuid) + + if (!vaultPermissions) { + return this.buildFailResult(operation, ConflictType.VaultNotMemberError) + } + + if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, vaultPermissions)) { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if (vaultPermissions === 'read') { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem) + if (!usesValidKey) { + return this.buildFailResult(operation, ConflictType.VaultInvalidItemsKey) + } + + return this.buildSuccessValue() + } + + private async handleCreateToVaultOperation(operation: CreateToVaultSaveOperation): Promise { + const vaultPermissions = await this.getVaultPermissions(operation.userUuid, operation.vaultUuid) + + if (!vaultPermissions) { + return this.buildFailResult(operation, ConflictType.VaultNotMemberError) + } + + if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, vaultPermissions)) { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + if (vaultPermissions === 'read') { + return this.buildFailResult(operation, ConflictType.VaultInsufficientPermissionsError) + } + + const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem) + if (!usesValidKey) { + return this.buildFailResult(operation, ConflictType.VaultInvalidItemsKey) + } + + return this.buildSuccessValue() + } + + private async incomingItemUsesValidItemsKey(itemHash: ItemHash): Promise { + const isItemNotEncryptedByItemsKey = itemHash.content_type === ContentType.VaultItemsKey + if (isItemNotEncryptedByItemsKey) { + return true + } + + const vault = await this.vaultService.getVault({ vaultUuid: itemHash.vault_uuid as string }) + + if (!vault) { + return false + } + + return itemHash.items_key_id === vault.specifiedItemsKeyUuid + } + + private async getVaultPermissions(userUuid: string, vaultUuid: string): Promise { + const vaultUser = await this.vaultUserService.getUserForVault({ + userUuid, + vaultUuid: vaultUuid, + }) + + if (vaultUser) { + return vaultUser.permissions + } + + return undefined + } +} diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/VaultSaveOperation.ts b/packages/syncing-server/src/Domain/Item/SaveRule/VaultSaveOperation.ts new file mode 100644 index 000000000..763747a37 --- /dev/null +++ b/packages/syncing-server/src/Domain/Item/SaveRule/VaultSaveOperation.ts @@ -0,0 +1,44 @@ +import { ItemHash } from '../ItemHash' +import { Item } from '../Item' + +type SaveOperationCommon = { + incomingItem: ItemHash + userUuid: string +} + +export type AddToVaultSaveOperation = SaveOperationCommon & { + type: 'add-to-vault' + vaultUuid: string + existingItem: Item +} + +export type RemoveFromVaultSaveOperation = SaveOperationCommon & { + type: 'remove-from-vault' + vaultUuid: string + existingItem: Item +} + +export type MoveToOtherVaultSaveOperation = SaveOperationCommon & { + type: 'move-to-other-vault' + sourceVaultUuid: string + targetVaultUuid: string + existingItem: Item +} + +export type SaveToVaultSaveOperation = SaveOperationCommon & { + type: 'save-to-vault' + vaultUuid: string + existingItem: Item +} + +export type CreateToVaultSaveOperation = SaveOperationCommon & { + type: 'create-to-vault' + vaultUuid: string +} + +export type VaultSaveOperation = + | AddToVaultSaveOperation + | RemoveFromVaultSaveOperation + | MoveToOtherVaultSaveOperation + | SaveToVaultSaveOperation + | CreateToVaultSaveOperation diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/SnjsVersionFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/VaultSnjsVersionFilter.ts similarity index 79% rename from packages/syncing-server/src/Domain/Item/SaveRule/SnjsVersionFilter.ts rename to packages/syncing-server/src/Domain/Item/SaveRule/VaultSnjsVersionFilter.ts index 2fe6483d1..777d7939a 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/SnjsVersionFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/VaultSnjsVersionFilter.ts @@ -1,12 +1,12 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' import { lt } from 'semver' +import { ConflictType } from '../../../Tmp/ConflictType' const MINIMUM_SNJS_VERSION_FOR_VAULT_OPERATIONS = '2.200.0' -export class SnjsVersionFilter implements ItemSaveRuleInterface { +export class VaultSnjsVersionFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { const successValue = { passed: true, @@ -18,13 +18,13 @@ export class SnjsVersionFilter implements ItemSaveRuleInterface { } const isClientSnjsVersionLessThanMinimum = lt(dto.snjsVersion, MINIMUM_SNJS_VERSION_FOR_VAULT_OPERATIONS) - if (isClientSnjsVersionLessThanMinimum) { return { passed: false, conflict: { unsavedItem: dto.itemHash, - type: ConflictType.ContentError, + serverItem: dto.existingItem ?? undefined, + type: ConflictType.SnjsVersionError, }, } } diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts index dd716caa3..9e40fefae 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts @@ -1,7 +1,6 @@ -import { ConflictType } from '@standardnotes/responses' - import { ItemHash } from '../ItemHash' import { ItemProjection } from '../../../Projection/ItemProjection' +import { ConflictType } from '../../../Tmp/ConflictType' export type SyncResponse20161215 = { retrieved_items: Array diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts index 857fc6e5f..430259159 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts @@ -6,7 +6,7 @@ import { ItemHash } from '../ItemHash' import { ItemProjection } from '../../../Projection/ItemProjection' import { SyncResponse20161215 } from './SyncResponse20161215' import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface' -import { ConflictType } from '@standardnotes/responses' +import { ConflictType } from '../../../Tmp/ConflictType' export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface { private readonly LEGACY_MIN_CONFLICT_INTERVAL = 20_000_000 diff --git a/packages/syncing-server/src/Projection/ItemConflictProjection.ts b/packages/syncing-server/src/Projection/ItemConflictProjection.ts index 374f13593..f74b63446 100644 --- a/packages/syncing-server/src/Projection/ItemConflictProjection.ts +++ b/packages/syncing-server/src/Projection/ItemConflictProjection.ts @@ -1,6 +1,5 @@ -import { ConflictType } from '@standardnotes/responses' - import { ItemHash } from '../Domain/Item/ItemHash' +import { ConflictType } from '../Tmp/ConflictType' import { ItemProjection } from './ItemProjection' export type ItemConflictProjection = { diff --git a/packages/syncing-server/src/Tmp/ConflictType.ts b/packages/syncing-server/src/Tmp/ConflictType.ts new file mode 100644 index 000000000..a9406bf5c --- /dev/null +++ b/packages/syncing-server/src/Tmp/ConflictType.ts @@ -0,0 +1,14 @@ +export enum ConflictType { + ConflictingData = 'sync_conflict', + UuidConflict = 'uuid_conflict', + ContentTypeError = 'content_type_error', + ContentError = 'content_error', + ReadOnlyError = 'readonly_error', + UuidError = 'uuid_error', + SnjsVersionError = 'snjs_version_error', + VaultInsufficientPermissionsError = 'vault_insufficient_permissions_error', + VaultNotMemberError = 'vault_not_member_error', + VaultNotFoundError = 'vault_not_found_error', + VaultInvalidState = 'vault_invalid_state', + VaultInvalidItemsKey = 'vault_invalid_items_key', +}