Compare commits

..

4 Commits

Author SHA1 Message Date
standardci
1fa4b7cf27 chore(release): publish new version
- @standardnotes/home-server@1.11.22
 - @standardnotes/syncing-server@1.50.0
2023-07-03 17:55:21 +00:00
Karol Sójko
5dc5507039 feat: add invite users to a shared vault. (#636)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-03 19:40:36 +02:00
standardci
3035a20b9f chore(release): publish new version
- @standardnotes/home-server@1.11.21
 - @standardnotes/syncing-server@1.49.0
2023-07-03 16:58:01 +00:00
Karol Sójko
04b3bb034f feat: add creating shared vault file valet tokens. (#635)
Co-authored-by: Mo <mo@standardnotes.com>
2023-07-03 18:43:32 +02:00
16 changed files with 851 additions and 6 deletions

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.21...@standardnotes/home-server@1.11.22) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.20...@standardnotes/home-server@1.11.21) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.19...@standardnotes/home-server@1.11.20) (2023-07-03)
**Note:** Version bump only for package @standardnotes/home-server

View File

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

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.50.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.49.0...@standardnotes/syncing-server@1.50.0) (2023-07-03)
### Features
* add invite users to a shared vault. ([#636](https://github.com/standardnotes/syncing-server-js/issues/636)) ([5dc5507](https://github.com/standardnotes/syncing-server-js/commit/5dc5507039c0dfb9df82a85377846651fef73c57))
# [1.49.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.48.0...@standardnotes/syncing-server@1.49.0) (2023-07-03)
### Features
* add creating shared vault file valet tokens. ([#635](https://github.com/standardnotes/syncing-server-js/issues/635)) ([04b3bb0](https://github.com/standardnotes/syncing-server-js/commit/04b3bb034fb5bf6f9d00d5b2e8a1abc4832c5417))
# [1.48.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.47.0...@standardnotes/syncing-server@1.48.0) (2023-07-03)
### Features

View File

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

View File

@@ -6,4 +6,5 @@ export interface SharedVaultInviteRepositoryInterface {
findByUuid(sharedVaultInviteUuid: Uuid): Promise<SharedVaultInvite | null>
save(sharedVaultInvite: SharedVaultInvite): Promise<void>
remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
}

View File

@@ -3,6 +3,12 @@ import { Result, ValueObject } from '@standardnotes/domain-core'
import { SharedVaultUserPermissionProps } from './SharedVaultUserPermissionProps'
export class SharedVaultUserPermission extends ValueObject<SharedVaultUserPermissionProps> {
static readonly PERMISSIONS = {
Read: 'read',
Write: 'write',
Admin: 'admin',
}
get value(): string {
return this.props.value
}
@@ -12,7 +18,8 @@ export class SharedVaultUserPermission extends ValueObject<SharedVaultUserPermis
}
static create(sharedVaultUserPermission: string): Result<SharedVaultUserPermission> {
if (!['read', 'write', 'admin'].includes(sharedVaultUserPermission)) {
const isValidPermission = Object.values(this.PERMISSIONS).includes(sharedVaultUserPermission)
if (!isValidPermission) {
return Result.fail<SharedVaultUserPermission>(`Invalid shared vault user permission ${sharedVaultUserPermission}`)
} else {
return Result.ok<SharedVaultUserPermission>(new SharedVaultUserPermission({ value: sharedVaultUserPermission }))

View File

@@ -6,4 +6,5 @@ export interface SharedVaultUserRepositoryInterface {
findByUuid(sharedVaultUserUuid: Uuid): Promise<SharedVaultUser | null>
save(sharedVaultUser: SharedVaultUser): Promise<void>
remove(sharedVault: SharedVaultUser): Promise<void>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
}

View File

@@ -0,0 +1,363 @@
import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { CreateSharedVaultFileValetToken } from './CreateSharedVaultFileValetToken'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
import { Timestamps, Uuid } from '@standardnotes/domain-core'
describe('CreateSharedVaultFileValetToken', () => {
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let tokenEncoder: TokenEncoderInterface<SharedVaultValetTokenData>
const valetTokenTTL = 3600
let sharedVault: SharedVault
let sharedVaultUser: SharedVaultUser
const createUseCase = () =>
new CreateSharedVaultFileValetToken(sharedVaultRepository, sharedVaultUserRepository, tokenEncoder, valetTokenTTL)
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
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 = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<SharedVaultValetTokenData>>
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('encoded-token')
})
it('should return error when shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: 'invalid-uuid',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-uuid',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when shared vault is not found', async () => {
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault not found')
})
it('should return error when shared vault user is not found', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Read,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault user not found')
})
it('should return error when shared vault user does not have permission', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Write,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have permission to perform this operation')
})
it('should create a shared vault file valet token', async () => {
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()
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Write,
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
describe('move operation', () => {
beforeEach(() => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(
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(),
)
})
it('should return error when move operation type is not specified', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Move operation type is required')
})
it('should return error when target uuid is missing on a shared-vault-to-shared-vault move operation', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault to shared vault move target uuid is required')
})
it('should return error when target uuid is invalid on a shared-vault-to-shared-vault move operation', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return error when target shared vault user is not found on a shared-vault-to-shared-vault move operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Shared vault target user not found')
})
it('should return error when target shared vault user does not have permission on a shared-vault-to-shared-vault move operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(
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(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have permission to perform this operation')
})
it('should create move valet token for shared-vault-to-shared-vault operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(
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(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
it('should create move valet token for shared-vault-to-user operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(
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(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'shared-vault-to-user',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
it('should create move valet token for user-to-shared-vault operation', async () => {
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockReturnValueOnce(
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(),
)
.mockReturnValueOnce(
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(),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
remoteIdentifier: 'remote-identifier',
operation: ValetTokenOperation.Move,
moveOperationType: 'user-to-shared-vault',
sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe('encoded-token')
})
})
})

View File

@@ -0,0 +1,124 @@
import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
constructor(
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private tokenEncoder: TokenEncoderInterface<SharedVaultValetTokenData>,
private valetTokenTTL: number,
) {}
async execute(dto: CreateSharedVaultFileValetTokenDTO): Promise<Result<string>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Shared vault not found')
}
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid: userUuid,
sharedVaultUuid: sharedVaultUuid,
})
if (!sharedVaultUser) {
return Result.fail('Shared vault user not found')
}
if (
sharedVaultUser.props.permission.value === SharedVaultUserPermission.PERMISSIONS.Read &&
dto.operation !== ValetTokenOperation.Read
) {
return Result.fail('User does not have permission to perform this operation')
}
if (dto.operation === ValetTokenOperation.Move) {
if (!dto.moveOperationType) {
return Result.fail('Move operation type is required')
}
if (dto.moveOperationType === 'shared-vault-to-shared-vault') {
if (!dto.sharedVaultToSharedVaultMoveTargetUuid) {
return Result.fail('Shared vault to shared vault move target uuid is required')
}
const sharedVaultTargetUuidOrError = Uuid.create(dto.sharedVaultToSharedVaultMoveTargetUuid)
if (sharedVaultTargetUuidOrError.isFailed()) {
return Result.fail(sharedVaultTargetUuidOrError.getError())
}
const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue()
const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
userUuid: userUuid,
sharedVaultUuid: sharedVaultTargetUuid,
})
if (!toSharedVaultUser) {
return Result.fail('Shared vault target user not found')
}
if (toSharedVaultUser.props.permission.value === SharedVaultUserPermission.PERMISSIONS.Read) {
return Result.fail('User does not have permission to perform this operation')
}
}
}
const tokenData: SharedVaultValetTokenData = {
sharedVaultUuid: dto.sharedVaultUuid,
permittedOperation: dto.operation,
remoteIdentifier: dto.remoteIdentifier,
uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
uploadBytesLimit: sharedVault.props.fileUploadBytesLimit,
unencryptedFileSize: dto.unencryptedFileSize,
moveOperation: this.createMoveOperationData(dto),
}
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
return Result.ok(valetToken)
}
private createMoveOperationData(dto: CreateSharedVaultFileValetTokenDTO): SharedVaultValetTokenData['moveOperation'] {
if (!dto.moveOperationType) {
return undefined
}
let fromUuid: string
let toUuid: string
switch (dto.moveOperationType) {
case 'shared-vault-to-user':
fromUuid = dto.sharedVaultUuid
toUuid = dto.userUuid
break
case 'user-to-shared-vault':
fromUuid = dto.userUuid
toUuid = dto.sharedVaultUuid
break
case 'shared-vault-to-shared-vault':
fromUuid = dto.sharedVaultUuid
toUuid = dto.sharedVaultToSharedVaultMoveTargetUuid as string
break
}
return {
type: dto.moveOperationType,
fromUuid,
toUuid,
}
}
}

View File

@@ -0,0 +1,12 @@
import { SharedVaultMoveType, ValetTokenOperation } from '@standardnotes/security'
export interface CreateSharedVaultFileValetTokenDTO {
userUuid: string
sharedVaultUuid: string
fileUuid?: string
remoteIdentifier: string
operation: ValetTokenOperation
unencryptedFileSize?: number
moveOperationType?: SharedVaultMoveType
sharedVaultToSharedVaultMoveTargetUuid?: string
}

View File

@@ -0,0 +1,191 @@
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
import { InviteUserToSharedVault } from './InviteUserToSharedVault'
import { SharedVault } from '../../SharedVault/SharedVault'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
import { Uuid, Timestamps, Result } from '@standardnotes/domain-core'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
describe('InviteUserToSharedVault', () => {
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
let timer: TimerInterface
let sharedVault: SharedVault
const createUseCase = () => new InviteUserToSharedVault(sharedVaultRepository, sharedVaultInviteRepository, timer)
beforeEach(() => {
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
sharedVaultInviteRepository.save = jest.fn()
sharedVaultInviteRepository.remove = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should return a failure result if the shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: 'invalid',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should return a failure result if the shared vault does not exist', async () => {
const useCase = createUseCase()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(undefined)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Attempting to invite a user to a non-existent shared vault')
})
it('should return a failure result if the sender uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: 'invalid',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should return a failure result if the recipient uuid is invalid', async () => {
const useCase = createUseCase()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: 'invalid',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
})
it('should remove an already existing invite', async () => {
const useCase = createUseCase()
sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest
.fn()
.mockResolvedValue({} as jest.Mocked<SharedVaultInvite>)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(false)
expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
})
it('should create a shared vault invite', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue().props.sharedVaultUuid.value).toBe('00000000-0000-0000-0000-000000000000')
})
it('should return a failure if the permission is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: 'invalid',
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid shared vault user permission invalid')
})
it('should return a failure if the sender is not the owner of the shared vault', async () => {
const useCase = createUseCase()
sharedVault = SharedVault.create({
fileUploadBytesLimit: 100,
fileUploadBytesUsed: 2,
userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000001',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Only the owner of a shared vault can invite users to it')
})
it('should return a failure if the shared vault invite could not be created', async () => {
const useCase = createUseCase()
const mockSharedVaultInvite = jest.spyOn(SharedVaultInvite, 'create')
mockSharedVaultInvite.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
senderUuid: '00000000-0000-0000-0000-000000000000',
recipientUuid: '00000000-0000-0000-0000-000000000000',
permission: SharedVaultUserPermission.PERMISSIONS.Read,
encryptedMessage: 'encryptedMessage',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Oops')
mockSharedVaultInvite.mockRestore()
})
})

View File

@@ -0,0 +1,77 @@
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
import { InviteUserToSharedVaultDTO } from './InviteUserToSharedVaultDTO'
import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
import { TimerInterface } from '@standardnotes/time'
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvite> {
constructor(
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
private timer: TimerInterface,
) {}
async execute(dto: InviteUserToSharedVaultDTO): Promise<Result<SharedVaultInvite>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const senderUuidOrError = Uuid.create(dto.senderUuid)
if (senderUuidOrError.isFailed()) {
return Result.fail(senderUuidOrError.getError())
}
const senderUuid = senderUuidOrError.getValue()
const recipientUuidOrError = Uuid.create(dto.recipientUuid)
if (recipientUuidOrError.isFailed()) {
return Result.fail(recipientUuidOrError.getError())
}
const recipientUuid = recipientUuidOrError.getValue()
const permissionOrError = SharedVaultUserPermission.create(dto.permission)
if (permissionOrError.isFailed()) {
return Result.fail(permissionOrError.getError())
}
const permission = permissionOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Attempting to invite a user to a non-existent shared vault')
}
if (sharedVault.props.userUuid.value !== senderUuid.value) {
return Result.fail('Only the owner of a shared vault can invite users to it')
}
const existingInvite = await this.sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid({
userUuid: recipientUuid,
sharedVaultUuid,
})
if (existingInvite) {
await this.sharedVaultInviteRepository.remove(existingInvite)
}
const sharedVaultInviteOrError = SharedVaultInvite.create({
encryptedMessage: dto.encryptedMessage,
userUuid: recipientUuid,
sharedVaultUuid,
senderUuid,
permission,
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
})
if (sharedVaultInviteOrError.isFailed()) {
return Result.fail(sharedVaultInviteOrError.getError())
}
const sharedVaultInvite = sharedVaultInviteOrError.getValue()
await this.sharedVaultInviteRepository.save(sharedVaultInvite)
return Result.ok(sharedVaultInvite)
}
}

View File

@@ -0,0 +1,7 @@
export interface InviteUserToSharedVaultDTO {
sharedVaultUuid: string
senderUuid: string
recipientUuid: string
encryptedMessage: string
permission: string
}

View File

@@ -11,6 +11,27 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
) {}
async findByUserUuidAndSharedVaultUuid(dto: {
userUuid: Uuid
sharedVaultUuid: Uuid
}): Promise<SharedVaultInvite | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_invite')
.where('shared_vault_invite.user_uuid = :uuid', {
uuid: dto.userUuid.value,
})
.andWhere('shared_vault_invite.shared_vault_uuid = :sharedVaultUuid', {
sharedVaultUuid: dto.sharedVaultUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(sharedVaultInvite: SharedVaultInvite): Promise<void> {
const persistence = this.mapper.toProjection(sharedVaultInvite)
@@ -21,7 +42,7 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_invite')
.where('shared_vault_invite.uuid = :uuid', {
uuid: uuid.toString(),
uuid: uuid.value,
})
.getOne()

View File

@@ -21,7 +21,7 @@ export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterf
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault')
.where('shared_vault.uuid = :uuid', {
uuid: uuid.toString(),
uuid: uuid.value,
})
.getOne()

View File

@@ -11,6 +11,27 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
) {}
async findByUserUuidAndSharedVaultUuid(dto: {
userUuid: Uuid
sharedVaultUuid: Uuid
}): Promise<SharedVaultUser | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.user_uuid = :userUuid', {
userUuid: dto.userUuid.value,
})
.andWhere('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
sharedVaultUuid: dto.sharedVaultUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(sharedVaultUser: SharedVaultUser): Promise<void> {
const persistence = this.mapper.toProjection(sharedVaultUser)
@@ -21,7 +42,7 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.uuid = :uuid', {
uuid: uuid.toString(),
uuid: uuid.value,
})
.getOne()