Compare commits

...

8 Commits

Author SHA1 Message Date
standardci
64759ec2da chore(release): publish new version
- @standardnotes/home-server@1.13.8
 - @standardnotes/syncing-server@1.70.0
2023-07-25 11:03:19 +00:00
Karol Sójko
5f7e768e64 feat(syncing-server): filtering items by shared vault permissions (#670)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-25 12:45:49 +02:00
standardci
4bc189f1c5 chore(release): publish new version
- @standardnotes/home-server@1.13.7
 - @standardnotes/syncing-server@1.69.0
2023-07-24 14:29:21 +00:00
Karol Sójko
71721ab198 feat(syncing-server): determin shared vault operation type (#669)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-24 15:29:56 +02:00
standardci
5536a48966 chore(release): publish new version
- @standardnotes/home-server@1.13.6
 - @standardnotes/syncing-server@1.68.4
2023-07-24 09:15:15 +00:00
Karol Sójko
f77e29d3c9 fix(syncing-server): force remove shared vault owner when removing shared vault 2023-07-24 10:59:03 +02:00
standardci
4b1fc718a2 chore(release): publish new version
- @standardnotes/analytics@2.25.3
 - @standardnotes/api-gateway@1.67.2
 - @standardnotes/auth-server@1.126.2
 - @standardnotes/domain-core@1.23.3
 - @standardnotes/event-store@1.11.10
 - @standardnotes/files-server@1.19.12
 - @standardnotes/home-server@1.13.5
 - @standardnotes/revisions-server@1.25.3
 - @standardnotes/scheduler-server@1.20.12
 - @standardnotes/settings@1.21.17
 - @standardnotes/syncing-server@1.68.3
 - @standardnotes/websockets-server@1.10.5
2023-07-21 12:03:05 +00:00
Karol Sójko
1708c3f8a0 fix(domain-core): notification payload creation from string 2023-07-21 13:44:44 +02:00
49 changed files with 1841 additions and 92 deletions

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.2...@standardnotes/analytics@2.25.3) (2023-07-21)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.1...@standardnotes/analytics@2.25.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.2",
"version": "2.25.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.67.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.1...@standardnotes/api-gateway@1.67.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.67.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.0...@standardnotes/api-gateway@1.67.1) (2023-07-21)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.67.1",
"version": "1.67.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.126.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.1...@standardnotes/auth-server@1.126.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.126.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.0...@standardnotes/auth-server@1.126.1) (2023-07-21)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.126.1",
"version": "1.126.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.23.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.2...@standardnotes/domain-core@1.23.3) (2023-07-21)
### Bug Fixes
* **domain-core:** notification payload creation from string ([1708c3f](https://github.com/standardnotes/server/commit/1708c3f8a00897369585e1ef8022950a7c3c610e))
## [1.23.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.1...@standardnotes/domain-core@1.23.2) (2023-07-21)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.23.2",
"version": "1.23.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,7 @@ import { Result } from '../Core/Result'
import { NotificationPayloadProps } from './NotificationPayloadProps'
import { NotificationType } from './NotificationType'
import { Uuid } from '../Common/Uuid'
export class NotificationPayload extends ValueObject<NotificationPayloadProps> {
private constructor(props: NotificationPayloadProps) {
@@ -10,14 +11,45 @@ export class NotificationPayload extends ValueObject<NotificationPayloadProps> {
}
override toString(): string {
return JSON.stringify(this.props)
return JSON.stringify({
version: this.props.version,
type: this.props.type.value,
sharedVaultUuid: this.props.sharedVaultUuid.value,
itemUuid: this.props.itemUuid ? this.props.itemUuid.value : undefined,
})
}
static createFromString(jsonPayload: string): Result<NotificationPayload> {
try {
const props = JSON.parse(jsonPayload)
return NotificationPayload.create(props)
const typeOrError = NotificationType.create(props.type)
if (typeOrError.isFailed()) {
return Result.fail<NotificationPayload>(typeOrError.getError())
}
const type = typeOrError.getValue()
const sharedVaultUuidOrError = Uuid.create(props.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail<NotificationPayload>(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
let itemUuid: Uuid | undefined = undefined
if (props.itemUuid) {
const itemUuidOrError = Uuid.create(props.itemUuid)
if (itemUuidOrError.isFailed()) {
return Result.fail<NotificationPayload>(itemUuidOrError.getError())
}
itemUuid = itemUuidOrError.getValue()
}
return NotificationPayload.create({
version: props.version,
type,
sharedVaultUuid,
itemUuid,
})
} catch (error) {
return Result.fail<NotificationPayload>((error as Error).message)
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.10](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.9...@standardnotes/event-store@1.11.10) (2023-07-21)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.9](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.8...@standardnotes/event-store@1.11.9) (2023-07-21)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.9",
"version": "1.11.10",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.12](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.11...@standardnotes/files-server@1.19.12) (2023-07-21)
**Note:** Version bump only for package @standardnotes/files-server
## [1.19.11](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.10...@standardnotes/files-server@1.19.11) (2023-07-21)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.19.11",
"version": "1.19.12",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.7...@standardnotes/home-server@1.13.8) (2023-07-25)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.7](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.6...@standardnotes/home-server@1.13.7) (2023-07-24)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.6](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.5...@standardnotes/home-server@1.13.6) (2023-07-24)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.5](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.4...@standardnotes/home-server@1.13.5) (2023-07-21)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.3...@standardnotes/home-server@1.13.4) (2023-07-21)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.4",
"version": "1.13.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.25.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.2...@standardnotes/revisions-server@1.25.3) (2023-07-21)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.1...@standardnotes/revisions-server@1.25.2) (2023-07-21)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.25.2",
"version": "1.25.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.12](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.11...@standardnotes/scheduler-server@1.20.12) (2023-07-21)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.11](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.10...@standardnotes/scheduler-server@1.20.11) (2023-07-21)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.11",
"version": "1.20.12",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.17](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.16...@standardnotes/settings@1.21.17) (2023-07-21)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.16](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.15...@standardnotes/settings@1.21.16) (2023-07-21)
**Note:** Version bump only for package @standardnotes/settings

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.21.16",
"version": "1.21.17",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.70.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.69.0...@standardnotes/syncing-server@1.70.0) (2023-07-25)
### Features
* **syncing-server:** filtering items by shared vault permissions ([#670](https://github.com/standardnotes/syncing-server-js/issues/670)) ([5f7e768](https://github.com/standardnotes/syncing-server-js/commit/5f7e768e64da0452e6efcf70e36cb5e867291456))
# [1.69.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.4...@standardnotes/syncing-server@1.69.0) (2023-07-24)
### Features
* **syncing-server:** determin shared vault operation type ([#669](https://github.com/standardnotes/syncing-server-js/issues/669)) ([71721ab](https://github.com/standardnotes/syncing-server-js/commit/71721ab1982b65feb4c84b44b267a249b573c537))
## [1.68.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.3...@standardnotes/syncing-server@1.68.4) (2023-07-24)
### Bug Fixes
* **syncing-server:** force remove shared vault owner when removing shared vault ([f77e29d](https://github.com/standardnotes/syncing-server-js/commit/f77e29d3c9c9a28be3c5624d2c9bf0ffd6348377))
## [1.68.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.2...@standardnotes/syncing-server@1.68.3) (2023-07-21)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.68.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.1...@standardnotes/syncing-server@1.68.2) (2023-07-21)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.68.2",
"version": "1.70.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -150,6 +150,8 @@ import { MessageHttpMapper } from '../Mapping/Http/MessageHttpMapper'
import { GetUserNotifications } from '../Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications'
import { NotificationHttpMapper } from '../Mapping/Http/NotificationHttpMapper'
import { NotificationHttpRepresentation } from '../Mapping/Http/NotificationHttpRepresentation'
import { DetermineSharedVaultOperationOnItem } from '../Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
import { SharedVaultFilter } from '../Domain/Item/SaveRule/SharedVaultFilter'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -494,12 +496,24 @@ export class ContainerConfigLoader {
.bind<TokenEncoderInterface<SharedVaultValetTokenData>>(TYPES.Sync_SharedVaultValetTokenEncoder)
.toConstantValue(new TokenEncoder<SharedVaultValetTokenData>(container.get(TYPES.Sync_VALET_TOKEN_SECRET)))
container
.bind<DetermineSharedVaultOperationOnItem>(TYPES.Sync_DetermineSharedVaultOperationOnItem)
.toConstantValue(new DetermineSharedVaultOperationOnItem())
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
container
.bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
.toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
container
.bind<SharedVaultFilter>(TYPES.Sync_SharedVaultFilter)
.toConstantValue(
new SharedVaultFilter(
container.get(TYPES.Sync_DetermineSharedVaultOperationOnItem),
container.get(TYPES.Sync_SharedVaultUserRepository),
),
)
container
.bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
.toConstantValue(
@@ -508,6 +522,7 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_TimeDifferenceFilter),
container.get(TYPES.Sync_ContentTypeFilter),
container.get(TYPES.Sync_ContentFilter),
container.get(TYPES.Sync_SharedVaultFilter),
]),
)

View File

@@ -76,6 +76,7 @@ const TYPES = {
Sync_GetItems: Symbol.for('Sync_GetItems'),
Sync_SaveItems: Symbol.for('Sync_SaveItems'),
Sync_GetUserNotifications: Symbol.for('Sync_GetUserNotifications'),
Sync_DetermineSharedVaultOperationOnItem: Symbol.for('Sync_DetermineSharedVaultOperationOnItem'),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
@@ -98,6 +99,7 @@ const TYPES = {
Sync_ItemBackupService: Symbol.for('Sync_ItemBackupService'),
Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),
Sync_SharedVaultFilter: Symbol.for('Sync_SharedVaultFilter'),
Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'),
Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'),
Sync_ContentFilter: Symbol.for('Sync_ContentFilter'),

View File

@@ -1,6 +1,7 @@
import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Item } from './Item'
import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
describe('Item', () => {
it('should create an aggregate', () => {
@@ -44,4 +45,56 @@ describe('Item', () => {
expect(entityOrError.isFailed()).toBeFalsy()
expect(() => entityOrError.getValue().uuid).toThrow()
})
it('should tell if an item is associated with a shared vault', () => {
const entityOrError = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(
entityOrError
.getValue()
.isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
).toBeTruthy()
})
it('should tell that an item is not associated with a shared vault', () => {
const entityOrError = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
})
expect(entityOrError.isFailed()).toBeFalsy()
expect(
entityOrError
.getValue()
.isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
).toBeFalsy()
})
})

View File

@@ -12,6 +12,34 @@ export class Item extends Aggregate<ItemProps> {
return uuidOrError.getValue()
}
get sharedVaultUuid(): Uuid | null {
if (!this.props.sharedVaultAssociation) {
return null
}
return this.props.sharedVaultAssociation.props.sharedVaultUuid
}
isAssociatedWithASharedVault(): boolean {
return this.sharedVaultUuid !== null
}
isAssociatedWithSharedVault(sharedVaultUuid: Uuid): boolean {
if (!this.isAssociatedWithASharedVault()) {
return false
}
return (this.sharedVaultUuid as Uuid).equals(sharedVaultUuid)
}
isAssociatedWithKeySystem(keySystemIdentifier: string): boolean {
if (!this.props.keySystemAssociation) {
return false
}
return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier
}
private constructor(props: ItemProps, id?: UniqueEntityId) {
super(props, id)
}

View File

@@ -0,0 +1,38 @@
import { ContentType } from '@standardnotes/domain-core'
import { ItemHash } from './ItemHash'
describe('ItemHash', () => {
it('should create a value object', () => {
const valueOrError = ItemHash.create({
uuid: '00000000-0000-0000-0000-000000000000',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: null,
})
expect(valueOrError.isFailed()).toBeFalsy()
})
it('should return error if shared vault uuid is not valid', () => {
const valueOrError = ItemHash.create({
uuid: '00000000-0000-0000-0000-000000000000',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: 'invalid',
})
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -1,4 +1,4 @@
import { Result, ValueObject } from '@standardnotes/domain-core'
import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
import { ItemHashProps } from './ItemHashProps'
@@ -8,6 +8,13 @@ export class ItemHash extends ValueObject<ItemHashProps> {
}
static create(props: ItemHashProps): Result<ItemHash> {
if (props.shared_vault_uuid) {
const sharedVaultUuidOrError = Uuid.create(props.shared_vault_uuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail<ItemHash>(sharedVaultUuidOrError.getError())
}
}
return Result.ok<ItemHash>(new ItemHash(props))
}
@@ -15,6 +22,14 @@ export class ItemHash extends ValueObject<ItemHashProps> {
return this.props.shared_vault_uuid !== null
}
get sharedVaultUuid(): Uuid | null {
if (!this.representsASharedVaultItem()) {
return null
}
return Uuid.create(this.props.shared_vault_uuid as string).getValue()
}
hasDedicatedKeySystemAssociation(): boolean {
return this.props.key_system_identifier !== null
}

View File

@@ -59,6 +59,31 @@ describe('OwnershipFilter', () => {
})
})
it('should deffer to the shared vault filter if the item hash represents a shared vault item or existing item is a shared vault item', async () => {
const itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
const result = await createFilter().check({
userUuid: '00000000-0000-0000-0000-000000000001',
apiVersion: ApiVersion.v20200115,
itemHash,
existingItem,
})
expect(result).toEqual({
passed: true,
})
})
it('should leave items belonging to the same user', async () => {
const result = await createFilter().check({
userUuid: '00000000-0000-0000-0000-000000000000',

View File

@@ -6,6 +6,14 @@ import { Uuid } from '@standardnotes/domain-core'
export class OwnershipFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const deferToSharedVaultFilter =
dto.existingItem?.isAssociatedWithASharedVault() || dto.itemHash.representsASharedVaultItem()
if (deferToSharedVaultFilter) {
return {
passed: true,
}
}
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return {

View File

@@ -0,0 +1,825 @@
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
import { SharedVaultFilter } from './SharedVaultFilter'
import { ItemHash } from '../ItemHash'
import { Item } from '../Item'
import { SharedVaultOperationOnItem } from '../../SharedVault/SharedVaultOperationOnItem'
import { SharedVaultAssociation } from '../../SharedVault/SharedVaultAssociation'
describe('SharedVaultFilter', () => {
let determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let sharedVaultUser: SharedVaultUser
let itemHash: ItemHash
let existingItem: Item
const createFilter = () => new SharedVaultFilter(determineSharedVaultOperationOnItem, sharedVaultUserRepository)
beforeEach(() => {
existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: false,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: 'key-system-identifier',
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
determineSharedVaultOperationOnItem.execute = jest.fn()
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockResolvedValueOnce(sharedVaultUser)
.mockResolvedValueOnce(null)
})
it('should return as passed if the item hash does not represent a shared vault item', async () => {
itemHash = ItemHash.create({
...itemHash.props,
shared_vault_uuid: null,
}).getValue()
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as passed if the item is not a shared vault item', async () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: undefined,
}).getValue()
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the operation could not be determined', async () => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(Result.fail('error'))
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the item is a shared vault item without a dedicated key system association', async () => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
incomingItemHash: itemHash,
}).getValue(),
),
)
itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
describe('when the shared vault operation on item is: move to other shared vault', () => {
beforeEach(() => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
incomingItemHash: itemHash,
}).getValue(),
),
)
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockResolvedValueOnce(sharedVaultUser)
.mockResolvedValueOnce(sharedVaultUser)
})
it('should return as not passed if the user is not a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user is not a member of the target shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockResolvedValue(sharedVaultUser)
.mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as passed if the user is a member of both shared vaults', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the user is not a member of the target shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(sharedVaultUser)
.mockReturnValueOnce(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the item is deleted', async () => {
existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: true,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the item is being deleted', async () => {
itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
deleted: true,
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
itemHash = ItemHash.create({
...itemHash.props,
content_type: ContentType.TYPES.KeySystemItemsKey,
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
incomingItemHash: itemHash,
}).getValue(),
),
)
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
})
describe('when the shared vault operation on item is: add to shared vault', () => {
beforeEach(() => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
})
it('should return as not passed if the user is not a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as passed if the user is a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the item is deleted', async () => {
existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: true,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user is not the owner of the item', async () => {
existingItem = Item.create({
...existingItem.props,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
itemHash = ItemHash.create({
...itemHash.props,
content_type: ContentType.TYPES.KeySystemItemsKey,
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
})
describe('when the shared vault operation on item is: remove from shared vault', () => {
beforeEach(() => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
})
it('should return as not passed if the user is not a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as passed if the user is a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the item is deleted', async () => {
existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: true,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user is not the owner of the item', async () => {
existingItem = Item.create({
...existingItem.props,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.passed).toBe(false)
})
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
itemHash = ItemHash.create({
...itemHash.props,
content_type: ContentType.TYPES.KeySystemItemsKey,
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
})
describe('when the shared vault operation on item is: save to shared vault', () => {
beforeEach(() => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.SaveToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
})
it('should return as not passed if the user is not a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as passed if the user is a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the user has insufficient permissions', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
})
describe('when the shared vault operation on item is: create to shared vault', () => {
beforeEach(() => {
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
incomingItemHash: itemHash,
existingItem,
}).getValue(),
),
)
})
it('should return as not passed if the user is not a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
it('should return as passed if the user is a member of the shared vault', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(true)
})
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
itemHash = ItemHash.create({
...itemHash.props,
content_type: ContentType.TYPES.KeySystemItemsKey,
}).getValue()
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
Result.ok(
SharedVaultOperationOnItem.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
incomingItemHash: itemHash,
}).getValue(),
),
)
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const filter = createFilter()
const result = await filter.check({
apiVersion: '001',
existingItem: existingItem,
itemHash: itemHash,
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.passed).toBe(false)
})
})
})

View File

@@ -0,0 +1,231 @@
import { ConflictType } from '@standardnotes/responses'
import { ContentType, Result, Uuid } from '@standardnotes/domain-core'
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
import { SharedVaultOperationOnItem } from '../../SharedVault/SharedVaultOperationOnItem'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
export class SharedVaultFilter implements ItemSaveRuleInterface {
constructor(
private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
) {}
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
if (!dto.itemHash.representsASharedVaultItem() || !dto.existingItem?.isAssociatedWithASharedVault()) {
return {
passed: true,
}
}
const operationOrError = await this.determineSharedVaultOperationOnItem.execute({
userUuid: dto.userUuid,
itemHash: dto.itemHash,
existingItem: dto.existingItem,
})
if (operationOrError.isFailed()) {
return {
passed: false,
conflict: {
unsavedItem: dto.itemHash,
type: ConflictType.SharedVaultInvalidState,
},
}
}
const operation = operationOrError.getValue()
if (dto.itemHash.representsASharedVaultItem() && !dto.itemHash.hasDedicatedKeySystemAssociation()) {
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
}
const sharedVaultPermission = await this.getSharedVaultUserPermission(
operation.props.userUuid,
operation.props.sharedVaultUuid,
)
if (!sharedVaultPermission) {
return this.buildFailResult(operation, ConflictType.SharedVaultNotMemberError)
}
let targetSharedVaultPermission: SharedVaultUserPermission | null = null
if (operation.props.targetSharedVaultUuid) {
targetSharedVaultPermission = await this.getSharedVaultUserPermission(
operation.props.userUuid,
operation.props.targetSharedVaultUuid,
)
if (!targetSharedVaultPermission) {
return this.buildFailResult(operation, ConflictType.SharedVaultNotMemberError)
}
}
const resultOrError = await this.getResultForOperation(
operation,
sharedVaultPermission,
targetSharedVaultPermission,
)
/* istanbul ignore next */
if (resultOrError.isFailed()) {
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
}
return resultOrError.getValue()
}
private async getResultForOperation(
operation: SharedVaultOperationOnItem,
sharedVaultPermission: SharedVaultUserPermission,
targetSharedVaultPermission: SharedVaultUserPermission | null,
): Promise<Result<ItemSaveRuleResult>> {
switch (operation.props.type) {
case SharedVaultOperationOnItem.TYPES.AddToSharedVault:
case SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault:
return Result.ok(await this.handleAddOrRemoveToSharedVaultOperation(operation, sharedVaultPermission))
case SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault:
return Result.ok(
await this.handleMoveToOtherSharedVaultOperation(
operation,
sharedVaultPermission,
targetSharedVaultPermission as SharedVaultUserPermission,
),
)
case SharedVaultOperationOnItem.TYPES.SaveToSharedVault:
case SharedVaultOperationOnItem.TYPES.CreateToSharedVault:
return Result.ok(await this.handleSaveOrCreateToSharedVaultOperation(operation, sharedVaultPermission))
/* istanbul ignore next */
default:
return Result.fail(`Unsupported sharedVault operation: ${operation}`)
}
}
private isAuthorizedToSaveContentType(contentType: string | null, permission: SharedVaultUserPermission): boolean {
if (contentType === ContentType.TYPES.KeySystemItemsKey) {
return permission.value === SharedVaultUserPermission.PERMISSIONS.Admin
}
return true
}
private async handleAddOrRemoveToSharedVaultOperation(
operation: SharedVaultOperationOnItem,
sharedVaultPermission: SharedVaultUserPermission,
): Promise<ItemSaveRuleResult> {
if (this.isItemDeletedOrBeingDeleted(operation)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
}
if (!this.isOwnerOfTheItem(operation)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
}
if (!this.hasSufficientPermissionsToWriteInVault(operation, sharedVaultPermission)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
}
return this.buildSuccessValue()
}
private async handleMoveToOtherSharedVaultOperation(
operation: SharedVaultOperationOnItem,
sourceSharedVaultPermission: SharedVaultUserPermission,
targetSharedVaultPermission: SharedVaultUserPermission,
): Promise<ItemSaveRuleResult> {
if (this.isItemDeletedOrBeingDeleted(operation)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
}
for (const permission of [sourceSharedVaultPermission, targetSharedVaultPermission]) {
if (!this.hasSufficientPermissionsToWriteInVault(operation, permission)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
}
}
return this.buildSuccessValue()
}
private async handleSaveOrCreateToSharedVaultOperation(
operation: SharedVaultOperationOnItem,
sharedVaultPermission: SharedVaultUserPermission,
): Promise<ItemSaveRuleResult> {
if (!this.hasSufficientPermissionsToWriteInVault(operation, sharedVaultPermission)) {
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
}
return this.buildSuccessValue()
}
private isItemDeletedOrBeingDeleted(operation: SharedVaultOperationOnItem): boolean {
if (operation.props.existingItem?.props.deleted || operation.props.incomingItemHash.props.deleted) {
return true
}
return false
}
private isOwnerOfTheItem(operation: SharedVaultOperationOnItem): boolean {
if (operation.props.userUuid.equals(operation.props.existingItem?.props.userUuid)) {
return true
}
return false
}
private hasSufficientPermissionsToWriteInVault(
operation: SharedVaultOperationOnItem,
sharedVaultPermission: SharedVaultUserPermission,
): boolean {
if (
!this.isAuthorizedToSaveContentType(operation.props.incomingItemHash.props.content_type, sharedVaultPermission)
) {
return false
}
if (sharedVaultPermission.value === SharedVaultUserPermission.PERMISSIONS.Read) {
return false
}
return true
}
private async getSharedVaultUserPermission(
userUuid: Uuid,
sharedVaultUuid: Uuid,
): Promise<SharedVaultUserPermission | null> {
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid,
sharedVaultUuid,
})
if (sharedVaultUser) {
return sharedVaultUser.props.permission
}
return null
}
private buildFailResult(operation: SharedVaultOperationOnItem, type: ConflictType): ItemSaveRuleResult {
const includeServerItem = [
ConflictType.SharedVaultInvalidState,
ConflictType.SharedVaultInsufficientPermissionsError,
].includes(type)
return {
passed: false,
conflict: {
unsavedItem: operation.props.incomingItemHash,
serverItem: includeServerItem ? operation.props.existingItem : undefined,
type,
},
}
}
private buildSuccessValue(): ItemSaveRuleResult {
return {
passed: true,
}
}
}

View File

@@ -0,0 +1,59 @@
import { ContentType, Uuid } from '@standardnotes/domain-core'
import { ItemHash } from '../Item/ItemHash'
import { SharedVaultOperationOnItem } from './SharedVaultOperationOnItem'
describe('SharedVaultOperationOnItem', () => {
let itemHash: ItemHash
beforeEach(() => {
itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: null,
}).getValue()
})
it('should create a value object', () => {
const valueOrError = SharedVaultOperationOnItem.create({
incomingItemHash: itemHash,
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(valueOrError.isFailed()).toBeFalsy()
})
it('should return error if shared vault operation type is invalid', () => {
const valueOrError = SharedVaultOperationOnItem.create({
incomingItemHash: itemHash,
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
type: 'invalid',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(valueOrError.isFailed()).toBeTruthy()
})
it('should return error if operation type is move to other shared vault and target shared vault uuid is not provided', () => {
const valueOrError = SharedVaultOperationOnItem.create({
incomingItemHash: itemHash,
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
targetSharedVaultUuid: undefined,
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,30 @@
import { ValueObject, Result } from '@standardnotes/domain-core'
import { SharedVaultOperationOnItemProps } from './SharedVaultOperationOnItemProps'
export class SharedVaultOperationOnItem extends ValueObject<SharedVaultOperationOnItemProps> {
static readonly TYPES = {
AddToSharedVault: 'add-to-shared-vault',
RemoveFromSharedVault: 'remove-from-shared-vault',
MoveToOtherSharedVault: 'move-to-other-shared-vault',
SaveToSharedVault: 'save-to-shared-vault',
CreateToSharedVault: 'create-to-shared-vault',
}
private constructor(props: SharedVaultOperationOnItemProps) {
super(props)
}
static create(props: SharedVaultOperationOnItemProps): Result<SharedVaultOperationOnItem> {
const isValidType = Object.values(this.TYPES).includes(props.type)
if (!isValidType) {
return Result.fail<SharedVaultOperationOnItem>(`Invalid shared vault operation type: ${props.type}`)
}
if (props.type === this.TYPES.MoveToOtherSharedVault && !props.targetSharedVaultUuid) {
return Result.fail<SharedVaultOperationOnItem>('Missing target shared vault uuid')
}
return Result.ok<SharedVaultOperationOnItem>(new SharedVaultOperationOnItem(props))
}
}

View File

@@ -0,0 +1,13 @@
import { Uuid } from '@standardnotes/domain-core'
import { Item } from '../Item/Item'
import { ItemHash } from '../Item/ItemHash'
export interface SharedVaultOperationOnItemProps {
incomingItemHash: ItemHash
userUuid: Uuid
type: string
sharedVaultUuid: Uuid
targetSharedVaultUuid?: Uuid
existingItem?: Item
}

View File

@@ -42,6 +42,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
originatorUuid: originatorUuid.value,
sharedVaultUuid: sharedVaultUuid.value,
userUuid: sharedVaultUser.props.userUuid.value,
forceRemoveOwner: true,
})
if (result.isFailed()) {

View File

@@ -0,0 +1,223 @@
import { ContentType, Uuid, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
import { Item } from '../../../Item/Item'
import { ItemHash } from '../../../Item/ItemHash'
import { DetermineSharedVaultOperationOnItem } from './DetermineSharedVaultOperationOnItem'
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
describe('DetermineSharedVaultOperationOnItem', () => {
let itemHash: ItemHash
let existingItem: Item
const createUseCase = () => new DetermineSharedVaultOperationOnItem()
beforeEach(() => {
itemHash = ItemHash.create({
uuid: '2-3-4',
content_type: ContentType.TYPES.Note,
user_uuid: '00000000-0000-0000-0000-000000000000',
content: 'foobar',
created_at: '2020-01-01T00:00:00.000Z',
updated_at: '2020-01-01T00:00:00.000Z',
created_at_timestamp: 123,
updated_at_timestamp: 123,
key_system_identifier: null,
shared_vault_uuid: null,
}).getValue()
existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: false,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
})
it('should return an error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
existingItem,
itemHash,
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('should return an operation representing moving to another shared vault', async () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
}).getValue(),
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault)
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
expect(result.getValue().props.targetSharedVaultUuid?.value).toEqual('00000000-0000-0000-0000-000000000001')
})
it('should return an operation representing removing from shared vault', async () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: null,
}).getValue(),
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault)
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
})
it('should return an operation representing adding to shared vault', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
}).getValue(),
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.AddToSharedVault)
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000001')
})
it('should return an operation representing saving to shared vault', async () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue(),
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.SaveToSharedVault)
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
})
it('should return an operation representing creating to shared vault', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem: null,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
}).getValue(),
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.CreateToSharedVault)
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000001')
})
it('should return an error if both existing and incoming item hash do not have shared vault uuid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem: null,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: null,
}).getValue(),
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid save operation')
})
it('should return error if operation could not be determined based on input values', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem: null,
itemHash,
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid save operation')
})
it('should return error if shared vault operation on item could not be created', async () => {
const mock = jest.spyOn(SharedVaultOperationOnItem, 'create')
mock.mockImplementationOnce(() => Result.fail('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
existingItem: null,
itemHash: ItemHash.create({
...itemHash.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
}).getValue(),
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
})
})

View File

@@ -0,0 +1,87 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
import { DetermineSharedVaultOperationOnItemDTO } from './DetermineSharedVaultOperationOnItemDTO'
import { Item } from '../../../Item/Item'
export class DetermineSharedVaultOperationOnItem implements UseCaseInterface<SharedVaultOperationOnItem> {
async execute(dto: DetermineSharedVaultOperationOnItemDTO): Promise<Result<SharedVaultOperationOnItem>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
let existingItemSharedVaultUuid = null
if (dto.existingItem) {
existingItemSharedVaultUuid = dto.existingItem.sharedVaultUuid
}
const targetItemSharedVaultUuid = dto.itemHash.sharedVaultUuid
if (!existingItemSharedVaultUuid && !targetItemSharedVaultUuid) {
return Result.fail('Invalid save operation')
}
const isMovingToOtherSharedVault =
dto.existingItem &&
existingItemSharedVaultUuid &&
targetItemSharedVaultUuid &&
!existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
const isRemovingFromSharedVault = dto.existingItem && existingItemSharedVaultUuid && !targetItemSharedVaultUuid
const isAddingToSharedVault = dto.existingItem && !existingItemSharedVaultUuid && targetItemSharedVaultUuid
const isSavingToSharedVault =
dto.existingItem &&
existingItemSharedVaultUuid &&
targetItemSharedVaultUuid &&
existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
let operationOrError: Result<SharedVaultOperationOnItem>
if (isMovingToOtherSharedVault) {
operationOrError = SharedVaultOperationOnItem.create({
existingItem: dto.existingItem as Item,
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
targetSharedVaultUuid: targetItemSharedVaultUuid,
incomingItemHash: dto.itemHash,
userUuid: userUuid,
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
})
} else if (isRemovingFromSharedVault) {
operationOrError = SharedVaultOperationOnItem.create({
existingItem: dto.existingItem as Item,
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
incomingItemHash: dto.itemHash,
userUuid: userUuid,
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
})
} else if (isAddingToSharedVault) {
operationOrError = SharedVaultOperationOnItem.create({
existingItem: dto.existingItem as Item,
sharedVaultUuid: targetItemSharedVaultUuid,
incomingItemHash: dto.itemHash,
userUuid: userUuid,
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
})
} else if (isSavingToSharedVault) {
operationOrError = SharedVaultOperationOnItem.create({
existingItem: dto.existingItem as Item,
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
incomingItemHash: dto.itemHash,
userUuid: userUuid,
type: SharedVaultOperationOnItem.TYPES.SaveToSharedVault,
})
} else {
operationOrError = SharedVaultOperationOnItem.create({
sharedVaultUuid: targetItemSharedVaultUuid as Uuid,
incomingItemHash: dto.itemHash,
userUuid: userUuid,
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
})
}
if (operationOrError.isFailed()) {
return Result.fail(operationOrError.getError())
}
return Result.ok(operationOrError.getValue())
}
}

View File

@@ -0,0 +1,8 @@
import { ItemHash } from '../../../Item/ItemHash'
import { Item } from '../../../Item/Item'
export interface DetermineSharedVaultOperationOnItemDTO {
userUuid: string
itemHash: ItemHash
existingItem: Item | null
}

View File

@@ -102,6 +102,26 @@ describe('RemoveUserFromSharedVault', () => {
expect(result.getError()).toBe('Only owner can remove users from shared vault')
})
it('should remove shared vault user if user is owner and is being force removed', async () => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
const useCase = createUseCase()
await useCase.execute({
originatorUuid: '00000000-0000-0000-0000-000000000002',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
userUuid: '00000000-0000-0000-0000-000000000001',
forceRemoveOwner: true,
})
expect(sharedVaultUserRepository.remove).toHaveBeenCalledWith(sharedVaultUser)
})
it('should return error when user is owner of shared vault', async () => {
const useCase = createUseCase()
const result = await useCase.execute({

View File

@@ -42,7 +42,7 @@ export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
}
const removingOwner = sharedVault.props.userUuid.equals(userUuid)
if (removingOwner) {
if (removingOwner && !dto.forceRemoveOwner) {
return Result.fail('Owner cannot be removed from shared vault')
}

View File

@@ -2,4 +2,5 @@ export interface RemoveUserFromSharedVaultDTO {
sharedVaultUuid: string
originatorUuid: string
userUuid: string
forceRemoveOwner?: boolean
}

View File

@@ -303,23 +303,6 @@ describe('SaveNewItem', () => {
})
describe('when item hash represents a shared vault item', () => {
it('returns a failure if the shared vault uuid is invalid', async () => {
const useCase = createUseCase()
itemHash1 = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: '1-2-3',
}).getValue()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
expect(result.isFailed()).toBeTruthy()
})
it('should create a shared vault association between the item and the shared vault', async () => {
const useCase = createUseCase()

View File

@@ -89,15 +89,9 @@ export class SaveNewItem implements UseCaseInterface<Item> {
let sharedVaultAssociation = undefined
if (dto.itemHash.representsASharedVaultItem()) {
const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
lastEditedBy: userUuid,
sharedVaultUuid,
sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),

View File

@@ -327,23 +327,6 @@ describe('UpdateExistingItem', () => {
expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore)
})
it('should return error if shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: 'invalid-uuid',
}).getValue()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if shared vault association could not be created', async () => {
const useCase = createUseCase()

View File

@@ -1,4 +1,13 @@
import { ContentType, Dates, Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
import {
ContentType,
Dates,
Result,
Timestamps,
UniqueEntityId,
UseCaseInterface,
Uuid,
Validator,
} from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
@@ -8,7 +17,6 @@ import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
import { ItemHash } from '../../../Item/ItemHash'
export class UpdateExistingItem implements UseCaseInterface<Item> {
constructor(
@@ -105,25 +113,26 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
if (
dto.itemHash.representsASharedVaultItem() &&
!this.itemIsAlreadyAssociatedWithTheSharedVault(dto.existingItem, dto.itemHash)
) {
const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
if (dto.itemHash.representsASharedVaultItem()) {
const sharedVaultAssociationOrError = SharedVaultAssociation.create(
{
lastEditedBy: userUuid,
sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
timestamps: Timestamps.create(
dto.existingItem.props.sharedVaultAssociation
? dto.existingItem.props.sharedVaultAssociation.props.timestamps.createdAt
: this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
},
new UniqueEntityId(
dto.existingItem.props.sharedVaultAssociation
? dto.existingItem.props.sharedVaultAssociation.id.toString()
: undefined,
),
)
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
lastEditedBy: userUuid,
sharedVaultUuid,
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
})
if (sharedVaultAssociationOrError.isFailed()) {
return Result.fail(sharedVaultAssociationOrError.getError())
}
@@ -133,7 +142,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
if (
dto.itemHash.hasDedicatedKeySystemAssociation() &&
!this.itemIsAlreadyAssociatedWithTheKeySystem(dto.existingItem, dto.itemHash)
!dto.existingItem.isAssociatedWithKeySystem(dto.itemHash.props.key_system_identifier as string)
) {
const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier)
if (keySystemIdentifiedValidationResult.isFailed()) {
@@ -192,18 +201,4 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
return Result.ok(dto.existingItem)
}
private itemIsAlreadyAssociatedWithTheSharedVault(item: Item, itemHash: ItemHash): boolean {
return (
item.props.sharedVaultAssociation !== undefined &&
item.props.sharedVaultAssociation.props.sharedVaultUuid.value === itemHash.props.shared_vault_uuid
)
}
private itemIsAlreadyAssociatedWithTheKeySystem(item: Item, itemHash: ItemHash): boolean {
return (
item.props.keySystemAssociation !== undefined &&
item.props.keySystemAssociation.props.keySystemIdentifier === itemHash.props.key_system_identifier
)
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.5](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.4...@standardnotes/websockets-server@1.10.5) (2023-07-21)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.10.4](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.3...@standardnotes/websockets-server@1.10.4) (2023-07-21)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.10.4",
"version": "1.10.5",
"engines": {
"node": ">=18.0.0 <21.0.0"
},