refactor(wip): vault system identifier

This commit is contained in:
Mo
2023-05-31 16:07:13 -05:00
parent 9323ca79c6
commit 07ae120e2a
34 changed files with 233 additions and 493 deletions
@@ -3,7 +3,7 @@ export enum ContentType {
Any = '*',
Item = 'SF|Item',
VaultItemsKey = 'SN|VaultItemsKey',
VaultKey = 'SN|VaultKey',
VaultKeyCopy = 'SN|VaultKeyCopy',
TrustedContact = 'SN|TrustedContact',
RootKey = 'SN|RootKey|NoSync',
ItemsKey = 'SN|ItemsKey',
@@ -44,7 +44,7 @@ export class CreateGroupInvites1684528646896 implements MigrationInterface {
isNullable: false,
}),
new TableColumn({
name: 'encrypted_group_data',
name: 'encrypted_vault_key_content',
type: 'text',
isNullable: false,
}),
@@ -1,4 +1,3 @@
import { GetGlobalItem } from './../Domain/UseCase/GetGlobalItem/GetGlobalItem'
import { ContactFactory } from './../Domain/Contact/Factory/ContactFactory'
import { GroupServiceInterface } from '../Domain/Group/Service/GroupServiceInterface'
@@ -114,9 +113,6 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
container.bind<GetItem>(TYPES.GetItem).toDynamicValue((context: interfaces.Context) => {
return new GetItem(context.container.get(TYPES.ItemRepository))
})
container.bind<GetGlobalItem>(TYPES.GetGlobalItem).toDynamicValue((context: interfaces.Context) => {
return new GetGlobalItem(context.container.get(TYPES.ItemRepository))
})
container
.bind<CreateGroupFileValetToken>(TYPES.CreateGroupFileReadValetToken)
@@ -148,7 +144,6 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.ItemProjector),
context.container.get(TYPES.MAX_ITEMS_LIMIT),
context.container.get(TYPES.GroupUserRepository),
context.container.get(TYPES.GroupService),
context.container.get(TYPES.Logger),
)
})
@@ -159,6 +154,7 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.GroupFactory),
context.container.get(TYPES.GroupUserService),
context.container.get(TYPES.GroupInviteService),
context.container.get(TYPES.ItemService),
context.container.get(TYPES.Timer),
)
})
@@ -74,7 +74,6 @@ const TYPES = {
SyncItems: Symbol.for('SyncItems'),
CheckIntegrity: Symbol.for('CheckIntegrity'),
GetItem: Symbol.for('GetItem'),
GetGlobalItem: Symbol.for('GetGlobalItem'),
CreateGroupFileReadValetToken: Symbol.for('CreateGroupFileReadValetToken'),
// Handlers
UserCredentialsChangedEventHandler: Symbol.for('UserCredentialsChangedEventHandler'),
@@ -32,7 +32,7 @@ export class GroupInvitesController extends BaseHttpController {
groupUuid: request.params.groupUuid,
userUuid: request.body.invitee_uuid,
inviterPublicKey: request.body.inviter_public_key,
encryptedGroupData: request.body.encrypted_group_data,
encryptedVaultKeyContent: request.body.encrypted_vault_key_content,
inviteType: request.body.invite_type,
permissions: request.body.permissions,
})
@@ -53,7 +53,7 @@ export class GroupInvitesController extends BaseHttpController {
originatorUuid: response.locals.user.uuid,
inviteUuid: request.params.inviteUuid,
inviterPublicKey: request.body.inviter_public_key,
encryptedGroupData: request.body.encrypted_group_data,
encryptedVaultKeyContent: request.body.encrypted_vault_key_content,
permissions: request.body.permissions,
})
@@ -18,6 +18,8 @@ import { GroupProjection } from '../Projection/GroupProjection'
import { GroupUserProjection } from '../Projection/GroupUserProjection'
import { CreateGroupFileValetToken } from '../Domain/UseCase/CreateGroupFileValetToken/CreateGroupFileValetToken'
import { RemovedGroupUserServiceInterface } from '../Domain/RemovedGroupUser/Service/RemovedGroupUserServiceInterface'
import { Item } from '../Domain/Item/Item'
import { ItemProjection } from '../Projection/ItemProjection'
@controller('/groups')
export class GroupsController extends BaseHttpController {
@@ -27,6 +29,7 @@ export class GroupsController extends BaseHttpController {
@inject(TYPES.GroupProjector) private groupProjector: ProjectorInterface<Group, GroupProjection>,
@inject(TYPES.GroupUserProjector) private groupUserProjector: ProjectorInterface<GroupUser, GroupUserProjection>,
@inject(TYPES.CreateGroupFileReadValetToken) private createGroupFileReadValetToken: CreateGroupFileValetToken,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
) {
super()
}
@@ -42,36 +45,11 @@ export class GroupsController extends BaseHttpController {
})
}
@httpPost('/:groupUuid/valet-tokens', TYPES.AuthMiddleware)
public async getValetTokenForGroupFile(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const valetTokenResult = await this.createGroupFileReadValetToken.execute({
userUuid: response.locals.user.uuid,
groupUuid: request.params.groupUuid,
fileUuid: request.body.file_uuid,
remoteIdentifier: request.body.remote_identifier,
operation: request.body.operation,
unencryptedFileSize: request.body.unencrypted_file_size,
moveOperationType: request.body.move_operation_type,
groupToGroupMoveTargetUuid: request.body.group_to_group_move_target_uuid,
})
if (valetTokenResult.success === false) {
return this.errorResponse(400, `Failed to create group valet token: ${valetTokenResult.reason}`)
}
return this.json({
valetToken: valetTokenResult.valetToken,
})
}
@httpPost('/', TYPES.AuthMiddleware)
public async createGroup(request: Request, response: Response): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.groupService.createGroup({
userUuid: response.locals.user.uuid,
groupUuid: request.body.group_uuid,
vaultSystemIdentifier: request.body.vault_system_identifier,
specifiedItemsKeyUuid: request.body.specified_items_key_uuid,
})
@@ -116,6 +94,46 @@ export class GroupsController extends BaseHttpController {
return this.json({ success: true })
}
@httpPost('/:groupUuid/items', TYPES.AuthMiddleware)
public async addItemToGroup(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.groupService.addItemToGroup({
groupUuid: request.params.groupUuid,
itemUuid: request.body.item_uuid,
originatorUuid: response.locals.user.uuid,
})
if (!result) {
return this.errorResponse(400, 'Could not add item to group')
}
return this.json({
item: this.itemProjector.projectFull(result),
})
}
@httpDelete('/:groupUuid/items', TYPES.AuthMiddleware)
public async removeItemFromGroup(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.groupService.removeItemFromGroup({
groupUuid: request.params.groupUuid,
itemUuid: request.body.item_uuid,
originatorUuid: response.locals.user.uuid,
})
if (!result) {
return this.errorResponse(400, 'Could not remove item from group')
}
return this.json({
item: this.itemProjector.projectFull(result),
})
}
@httpGet('/removed', TYPES.AuthMiddleware)
public async getAllRemovedFromGroupsForCurrentUser(
_request: Request,
@@ -139,6 +157,31 @@ export class GroupsController extends BaseHttpController {
})
}
@httpPost('/:groupUuid/valet-tokens', TYPES.AuthMiddleware)
public async getValetTokenForGroupFile(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const valetTokenResult = await this.createGroupFileReadValetToken.execute({
userUuid: response.locals.user.uuid,
groupUuid: request.params.groupUuid,
fileUuid: request.body.file_uuid,
remoteIdentifier: request.body.remote_identifier,
operation: request.body.operation,
unencryptedFileSize: request.body.unencrypted_file_size,
moveOperationType: request.body.move_operation_type,
groupToGroupMoveTargetUuid: request.body.group_to_group_move_target_uuid,
})
if (valetTokenResult.success === false) {
return this.errorResponse(400, `Failed to create group valet token: ${valetTokenResult.reason}`)
}
return this.json({
valetToken: valetTokenResult.valetToken,
})
}
private errorResponse(status: number, message?: string, tag?: string) {
return this.json(
{
@@ -11,6 +11,7 @@ export class GroupFactory implements GroupFactoryInterface {
newGroup.uuid = dto.groupHash.uuid
newGroup.userUuid = dto.userUuid
newGroup.specifiedItemsKeyUuid = dto.groupHash.specified_items_key_uuid
newGroup.vaultSystemIdentifier = dto.groupHash.vault_system_identifier
newGroup.fileUploadBytesUsed = dto.groupHash.file_upload_bytes_used
newGroup.fileUploadBytesLimit = dto.groupHash.file_upload_bytes_limit
@@ -2,6 +2,7 @@ export type GroupHash = {
uuid: string
user_uuid: string
specified_items_key_uuid: string
vault_system_identifier: string
file_upload_bytes_used: number
file_upload_bytes_limit: number
created_at_timestamp?: number
@@ -18,6 +18,12 @@ export class Group {
})
declare specifiedItemsKeyUuid: string
@Column({
name: 'vault_system_identifier',
length: 36,
})
declare vaultSystemIdentifier: string
@Column({
name: 'file_upload_bytes_used',
type: 'bigint',
@@ -1,3 +1,4 @@
import { ItemServiceInterface } from './../../Item/ItemServiceInterface'
import { TimerInterface } from '@standardnotes/time'
import { Group } from '../Model/Group'
import { GroupsRepositoryInterface } from '../Repository/GroupRepositoryInterface'
@@ -5,6 +6,8 @@ import { CreateGroupDTO, GroupServiceInterface, UpdateGroupDTO, CreateGroupResul
import { GroupFactoryInterface } from '../Factory/GroupFactoryInterface'
import { GroupUserServiceInterface } from '../../GroupUser/Service/GroupUserServiceInterface'
import { GroupInviteServiceInterface } from '../../GroupInvite/Service/GroupInviteServiceInterface'
import { v4 as uuidv4 } from 'uuid'
import { Item } from '../../Item/Item'
export class GroupService implements GroupServiceInterface {
constructor(
@@ -12,21 +15,18 @@ export class GroupService implements GroupServiceInterface {
private groupFactory: GroupFactoryInterface,
private groupUserService: GroupUserServiceInterface,
private groupInviteService: GroupInviteServiceInterface,
private itemService: ItemServiceInterface,
private timer: TimerInterface,
) {}
async createGroup(dto: CreateGroupDTO): Promise<CreateGroupResult | null> {
const existingGroup = await this.groupRepository.findByUuid(dto.groupUuid)
if (existingGroup) {
return null
}
async createGroup(dto: CreateGroupDTO): Promise<CreateGroupResult> {
const timestamp = this.timer.getTimestampInMicroseconds()
const group = this.groupFactory.create({
userUuid: dto.userUuid,
groupHash: {
uuid: dto.groupUuid,
uuid: uuidv4(),
user_uuid: dto.userUuid,
vault_system_identifier: dto.vaultSystemIdentifier,
specified_items_key_uuid: dto.specifiedItemsKeyUuid,
file_upload_bytes_limit: 1_000_000,
file_upload_bytes_used: 0,
@@ -87,10 +87,12 @@ export class GroupService implements GroupServiceInterface {
}
await this.groupRepository.remove(group)
await this.groupUserService.deleteAllGroupUsersForGroup({
groupUuid: dto.groupUuid,
originatorUuid: dto.originatorUuid,
})
await this.groupInviteService.deleteAllInvitesForGroup({
groupUuid: dto.groupUuid,
originatorUuid: dto.originatorUuid,
@@ -98,4 +100,46 @@ export class GroupService implements GroupServiceInterface {
return true
}
async addItemToGroup(dto: { itemUuid: string; groupUuid: string; originatorUuid: string }): Promise<Item | null> {
const item = await this.itemService.getItem({ itemUuid: dto.itemUuid, userUuid: dto.originatorUuid })
if (!item) {
return null
}
const groupUser = await this.groupUserService.getUserForGroup({
userUuid: dto.originatorUuid,
groupUuid: dto.groupUuid,
})
if (!groupUser || groupUser.permissions === 'read') {
return null
}
item.groupUuid = dto.groupUuid
return this.itemService.saveItem(item)
}
async removeItemFromGroup(dto: {
itemUuid: string
groupUuid: string
originatorUuid: string
}): Promise<Item | null> {
const item = await this.itemService.getItem({ itemUuid: dto.itemUuid, userUuid: dto.originatorUuid })
if (!item) {
return null
}
const groupUser = await this.groupUserService.getUserForGroup({
userUuid: dto.originatorUuid,
groupUuid: dto.groupUuid,
})
if (!groupUser || groupUser.permissions === 'read') {
return null
}
item.groupUuid = null
return this.itemService.saveItem(item)
}
}
@@ -1,10 +1,11 @@
import { GroupUser } from './../../GroupUser/Model/GroupUser'
import { Group } from '../Model/Group'
import { Item } from '../../Item/Item'
export type CreateGroupDTO = {
userUuid: string
groupUuid: string
specifiedItemsKeyUuid: string
vaultSystemIdentifier: string
}
export type CreateGroupResult = {
@@ -19,13 +20,13 @@ export type UpdateGroupDTO = {
}
export interface GroupServiceInterface {
createGroup(dto: CreateGroupDTO): Promise<CreateGroupResult | null>
createGroup(dto: CreateGroupDTO): Promise<CreateGroupResult>
updateGroup(dto: UpdateGroupDTO): Promise<Group | null>
deleteGroup(dto: { groupUuid: string; originatorUuid: string }): Promise<boolean>
getGroup(dto: { groupUuid: string }): Promise<Group | null>
getGroups(dto: { userUuid: string; lastSyncTime?: number }): Promise<Group[]>
addItemToGroup(dto: { itemUuid: string; groupUuid: string; originatorUuid: string }): Promise<Item | null>
removeItemFromGroup(dto: { itemUuid: string; groupUuid: string; originatorUuid: string }): Promise<Item | null>
}
@@ -13,7 +13,7 @@ export class GroupInviteFactory implements GroupInviteFactoryInterface {
newGroupInvite.groupUuid = dto.group_uuid
newGroupInvite.inviterUuid = dto.inviter_uuid
newGroupInvite.inviterPublicKey = dto.inviter_public_key
newGroupInvite.encryptedGroupData = dto.encrypted_group_data
newGroupInvite.encryptedVaultKeyContent = dto.encrypted_vault_key_content
newGroupInvite.inviteType = dto.invite_type
newGroupInvite.permissions = dto.permissions
@@ -7,7 +7,7 @@ export type GroupInviteHash = {
group_uuid: string
inviter_uuid: string
inviter_public_key: string
encrypted_group_data: string
encrypted_vault_key_content: string
invite_type: GroupInviteType
permissions: GroupUserPermission
created_at_timestamp?: number
@@ -35,9 +35,9 @@ export class GroupInvite {
declare inviterPublicKey: string
@Column({
name: 'encrypted_group_data',
name: 'encrypted_vault_key_content',
})
declare encryptedGroupData: string
declare encryptedVaultKeyContent: string
@Column({
name: 'permissions',
@@ -6,7 +6,7 @@ export type CreateInviteDTO = {
groupUuid: string
userUuid: string
inviterPublicKey: string
encryptedGroupData: string
encryptedVaultKeyContent: string
inviteType: GroupInviteType
permissions: GroupUserPermission
}
@@ -34,7 +34,7 @@ export class GroupInviteService implements GroupInviteServiceInterface {
group_uuid: dto.groupUuid,
inviter_uuid: dto.originatorUuid,
inviter_public_key: dto.inviterPublicKey,
encrypted_group_data: dto.encryptedGroupData,
encrypted_vault_key_content: dto.encryptedVaultKeyContent,
invite_type: dto.inviteType,
permissions: dto.permissions,
created_at_timestamp: timestamp,
@@ -51,7 +51,7 @@ export class GroupInviteService implements GroupInviteServiceInterface {
}
groupInvite.inviterPublicKey = dto.inviterPublicKey
groupInvite.encryptedGroupData = dto.encryptedGroupData
groupInvite.encryptedVaultKeyContent = dto.encryptedVaultKeyContent
if (dto.permissions) {
groupInvite.permissions = dto.permissions
}
@@ -5,22 +5,15 @@ import { UpdateInviteDTO } from './UpdateInviteDTO'
export interface GroupInviteServiceInterface {
createInvite(dto: CreateInviteDTO): Promise<GroupInvite | null>
updateInvite(dto: UpdateInviteDTO): Promise<GroupInvite | null>
getInvitesForUser(dto: GetUserGroupInvitesDTO): Promise<GroupInvite[]>
getInvitesForGroup(dto: { groupUuid: string; originatorUuid: string }): Promise<GroupInvite[] | undefined>
getOutboundInvitesForUser(dto: { userUuid: string }): Promise<GroupInvite[]>
acceptInvite(dto: { originatorUuid: string; inviteUuid: string }): Promise<boolean>
getInvitesForUser(dto: GetUserGroupInvitesDTO): Promise<GroupInvite[]>
getInvitesForGroup(dto: { groupUuid: string; originatorUuid: string }): Promise<GroupInvite[] | undefined>
getOutboundInvitesForUser(dto: { userUuid: string }): Promise<GroupInvite[]>
declineInvite(dto: { originatorUuid: string; inviteUuid: string }): Promise<boolean>
deleteInvite(dto: { originatorUuid: string; groupUuid: string; inviteUuid: string }): Promise<boolean>
deleteAllInvitesForGroup(dto: { originatorUuid: string; groupUuid: string }): Promise<boolean>
deleteAllInboundInvites(dto: { userUuid: string }): Promise<void>
}
@@ -4,6 +4,6 @@ export type UpdateInviteDTO = {
originatorUuid: string
inviteUuid: string
inviterPublicKey: string
encryptedGroupData: string
encryptedVaultKeyContent: string
permissions?: GroupUserPermission
}
+22 -28
View File
@@ -13,22 +13,6 @@ export class Item {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
type: 'varchar',
name: 'group_uuid',
length: 255,
nullable: true,
})
declare groupUuid: string | null
@Column({
type: 'varchar',
name: 'vault_identifier',
length: 255,
nullable: true,
})
declare vault_identifier: string | null
@Column({
type: 'varchar',
name: 'duplicate_of',
@@ -90,18 +74,6 @@ export class Item {
@Index('index_items_on_user_uuid')
declare userUuid: string
@Column({
name: 'last_edited_by_uuid',
length: 36,
})
declare lastEditedByUuid: string
@Column({
name: 'created_by_uuid',
length: 36,
})
declare createdByUuid: string
@Column({
type: 'tinyint',
precision: 1,
@@ -111,6 +83,28 @@ export class Item {
@Index('index_items_on_deleted')
declare deleted: boolean
@Column({
type: 'varchar',
name: 'vault_system_identifier',
length: 255,
nullable: true,
})
declare vaultSystemIdentifier: string | null
@Column({
type: 'varchar',
name: 'group_uuid',
length: 255,
nullable: true,
})
declare groupUuid: string | null
@Column({
name: 'last_edited_by_uuid',
length: 36,
})
declare lastEditedByUuid: string
@Column({
name: 'created_at',
type: 'datetime',
@@ -34,8 +34,8 @@ export class ItemFactory implements ItemFactoryInterface {
newItem.contentSize = 0
newItem.createdByUuid = dto.userUuid
if (dto.itemHash.group_uuid) {
newItem.groupUuid = dto.itemHash.group_uuid
if (dto.itemHash.vault_system_identifier) {
newItem.groupUuid = dto.itemHash.vault_system_identifier
} else {
newItem.userUuid = dto.userUuid
}
@@ -10,7 +10,7 @@ export type ItemHash = {
auth_hash?: string
enc_item_key?: string
items_key_id?: string
group_uuid?: string
vault_system_identifier?: string
last_edited_by_uuid?: string | null
created_at?: string
created_at_timestamp?: number
@@ -21,7 +21,6 @@ import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
import { GroupUserRepositoryInterface } from '../GroupUser/Repository/GroupUserRepositoryInterface'
import { ConflictType } from '../../Tmp/ConflictType'
import { GroupServiceInterface } from '../Group/Service/GroupServiceInterface'
export class ItemService implements ItemServiceInterface {
private readonly DEFAULT_ITEMS_LIMIT = 150
@@ -40,7 +39,6 @@ export class ItemService implements ItemServiceInterface {
private itemProjector: ProjectorInterface<Item, ItemProjection>,
private maxItemsSyncLimit: number,
private groupUsersRepository: GroupUserRepositoryInterface,
private groupService: GroupServiceInterface,
private logger: Logger,
) {}
@@ -99,8 +97,6 @@ export class ItemService implements ItemServiceInterface {
const savedItems: Array<Item> = []
const conflicts: Array<ItemConflict> = []
await this.dynamicallyCreateGroupsBasedOnIncomingItems(dto)
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
for (const itemHash of dto.itemHashes) {
@@ -187,30 +183,6 @@ export class ItemService implements ItemServiceInterface {
return retrievedItems
}
private async dynamicallyCreateGroupsBasedOnIncomingItems(dto: SaveItemsDTO): Promise<void> {
const processedGroupUuids = new Set()
for (const itemHash of dto.itemHashes) {
if (!itemHash.group_uuid || processedGroupUuids.has(itemHash.group_uuid)) {
continue
}
processedGroupUuids.add(itemHash.group_uuid)
const vaultItemsKeyUuid =
itemHash.content_type === ContentType.VaultItemsKey ? itemHash.uuid : itemHash.items_key_id
if (!vaultItemsKeyUuid) {
continue
}
await this.groupService.createGroup({
userUuid: dto.userUuid,
groupUuid: itemHash.group_uuid,
specifiedItemsKeyUuid: vaultItemsKeyUuid,
})
}
}
private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
if (savedItems.length) {
const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
@@ -238,12 +210,7 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.updatedWithSession = dto.sessionUuid
dto.existingItem.contentSize = 0
dto.existingItem.lastEditedByUuid = dto.userUuid
if (dto.itemHash.group_uuid) {
dto.existingItem.groupUuid = dto.itemHash.group_uuid
} else {
dto.existingItem.groupUuid = null
}
dto.existingItem.vaultSystemIdentifier = dto.itemHash.vault_system_identifier ?? null
if (dto.itemHash.content) {
dto.existingItem.content = dto.itemHash.content
@@ -360,4 +327,17 @@ export class ItemService implements ItemServiceInterface {
throw Error('Sync token is missing version part')
}
}
public async saveItem(item: Item): Promise<Item> {
const updatedAt = this.timer.getTimestampInMicroseconds()
item.updatedAtTimestamp = updatedAt
item.updatedAt = this.timer.convertMicrosecondsToDate(updatedAt)
const savedItem = await this.itemRepository.save(item)
return savedItem
}
public async getItem(dto: { itemUuid: string; userUuid: string }): Promise<Item | null> {
return this.itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)
}
}
@@ -9,4 +9,6 @@ export interface ItemServiceInterface {
saveItems(dto: SaveItemsDTO): Promise<SaveItemsResult>
frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>>
getLastSyncTime(dto: { syncToken?: string | null; cursorToken?: string | null }): number | undefined
getItem(dto: { itemUuid: string; userUuid: string }): Promise<Item | null>
saveItem(item: Item): Promise<Item>
}
@@ -7,106 +7,69 @@ import { GroupUserPermission } from '../../GroupUser/Model/GroupUserPermission'
import { GroupServiceInterface } from '../../Group/Service/GroupServiceInterface'
import { ContentType } from '@standardnotes/common'
import { ConflictType } from '../../../Tmp/ConflictType'
import {
GroupSaveOperation,
AddToGroupSaveOperation,
RemoveFromGroupSaveOperation,
MoveToOtherGroupSaveOperation,
SaveToGroupSaveOperation,
CreateToGroupSaveOperation,
} from './GroupSaveOperation'
import { Item } from '../Item'
export class GroupFilter implements ItemSaveRuleInterface {
constructor(private groupService: GroupServiceInterface, private groupUserService: GroupUserServiceInterface) {}
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
if (!dto.existingItem?.groupUuid && !dto.itemHash.group_uuid) {
if (!dto.existingItem?.groupUuid) {
return {
passed: true,
}
}
const operation = this.getSaveOperationType(dto)
return this.getResultForOperation(operation)
}
private async getResultForOperation(operation: GroupSaveOperation): Promise<ItemSaveRuleResult> {
if (operation.type === 'add-to-group') {
return this.handleAddToGroupOperation(operation)
} else if (operation.type === 'remove-from-group') {
return this.handleRemoveFromGroupOperation(operation)
} else if (operation.type === 'move-to-other-group') {
return this.handleMoveToOtherGroupOperation(operation)
} else if (operation.type === 'save-to-group') {
return this.handleSaveToGroupOperation(operation)
} else if (operation.type === 'create-to-group') {
return this.handleCreateToGroupOperation(operation)
}
throw new Error(`Unsupported group operation: ${operation}`)
}
private getSaveOperationType(dto: ItemSaveValidationDTO): GroupSaveOperation {
const existingItemGroupUuid = dto.existingItem?.groupUuid
const targetItemGroupUuid = dto.itemHash.group_uuid
const common = {
incomingItem: dto.itemHash,
const groupUser = await this.groupUserService.getUserForGroup({
userUuid: dto.userUuid,
}
groupUuid: dto.existingItem.groupUuid,
})
if (
dto.existingItem &&
existingItemGroupUuid &&
targetItemGroupUuid &&
existingItemGroupUuid !== targetItemGroupUuid
) {
if (!groupUser) {
return {
type: 'move-to-other-group',
groupUuid: existingItemGroupUuid,
targetGroupUuid: targetItemGroupUuid,
existingItem: dto.existingItem,
...common,
passed: false,
conflict: {
unsavedItem: dto.itemHash,
type: ConflictType.GroupNotMemberError,
},
}
}
if (dto.existingItem && existingItemGroupUuid && !targetItemGroupUuid) {
if (groupUser.permissions === 'read') {
return {
type: 'remove-from-group',
groupUuid: existingItemGroupUuid,
existingItem: dto.existingItem,
...common,
passed: false,
conflict: {
unsavedItem: dto.itemHash,
serverItem: dto.existingItem,
type: ConflictType.GroupInsufficientPermissionsError,
},
}
}
if (dto.existingItem && !existingItemGroupUuid && targetItemGroupUuid) {
if (!this.isAuthorizedToSaveContentType(dto.itemHash.content_type, groupUser.permissions)) {
return {
type: 'add-to-group',
groupUuid: targetItemGroupUuid,
existingItem: dto.existingItem,
...common,
passed: false,
conflict: {
unsavedItem: dto.itemHash,
serverItem: dto.existingItem,
type: ConflictType.GroupInsufficientPermissionsError,
},
}
}
if (dto.existingItem && existingItemGroupUuid && existingItemGroupUuid === targetItemGroupUuid) {
const usesValidKey = await this.incomingItemUsesValidItemsKey(dto.itemHash)
if (!usesValidKey) {
return {
type: 'save-to-group',
groupUuid: existingItemGroupUuid,
existingItem: dto.existingItem,
...common,
passed: false,
conflict: {
unsavedItem: dto.itemHash,
serverItem: dto.existingItem,
type: ConflictType.GroupInvalidItemsKey,
},
}
}
if (!dto.existingItem && targetItemGroupUuid) {
return {
type: 'create-to-group',
groupUuid: targetItemGroupUuid,
...common,
}
return {
passed: true,
}
throw new Error('Invalid save operation')
}
private isAuthorizedToSaveContentType(contentType: ContentType, permissions: GroupUserPermission): boolean {
@@ -117,167 +80,13 @@ export class GroupFilter implements ItemSaveRuleInterface {
return true
}
private buildFailResult(operation: GroupSaveOperation, type: ConflictType, serverItem?: Item) {
return {
passed: false,
conflict: {
unsavedItem: operation.incomingItem,
serverItem,
type,
},
}
}
private buildSuccessValue(): ItemSaveRuleResult {
return {
passed: true,
}
}
private async handleAddToGroupOperation(operation: AddToGroupSaveOperation): Promise<ItemSaveRuleResult> {
const groupPermissions = await this.getGroupPermissions(operation.userUuid, operation.groupUuid)
if (!groupPermissions) {
return this.buildFailResult(operation, ConflictType.GroupNotMemberError)
}
if (operation.existingItem.deleted || operation.incomingItem.deleted) {
return this.buildFailResult(operation, ConflictType.GroupInvalidState)
}
if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, groupPermissions)) {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (groupPermissions === 'read') {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (operation.existingItem.userUuid !== operation.userUuid) {
return this.buildFailResult(operation, ConflictType.UuidConflict)
}
const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem)
if (!usesValidKey) {
return this.buildFailResult(operation, ConflictType.GroupInvalidItemsKey, operation.existingItem)
}
return this.buildSuccessValue()
}
private async handleRemoveFromGroupOperation(operation: RemoveFromGroupSaveOperation): Promise<ItemSaveRuleResult> {
const groupPermissions = await this.getGroupPermissions(operation.userUuid, operation.groupUuid)
if (!groupPermissions) {
return this.buildFailResult(operation, ConflictType.GroupNotMemberError)
}
if (operation.existingItem.deleted || operation.incomingItem.deleted) {
return this.buildFailResult(operation, ConflictType.GroupInvalidState)
}
if (operation.existingItem.userUuid === operation.userUuid) {
return this.buildSuccessValue()
}
if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, groupPermissions)) {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (groupPermissions === 'read') {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem)
if (!usesValidKey) {
return this.buildFailResult(operation, ConflictType.GroupInvalidItemsKey, operation.existingItem)
}
return this.buildSuccessValue()
}
private async handleMoveToOtherGroupOperation(operation: MoveToOtherGroupSaveOperation): Promise<ItemSaveRuleResult> {
const sourceGroupPermissions = await this.getGroupPermissions(operation.userUuid, operation.groupUuid)
const targetGroupPermissions = await this.getGroupPermissions(operation.userUuid, operation.targetGroupUuid)
if (!sourceGroupPermissions || !targetGroupPermissions) {
return this.buildFailResult(operation, ConflictType.GroupNotMemberError)
}
if (operation.existingItem.deleted || operation.incomingItem.deleted) {
return this.buildFailResult(operation, ConflictType.GroupInvalidState)
}
if (sourceGroupPermissions === 'read' || targetGroupPermissions === 'read') {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (
!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, sourceGroupPermissions) ||
!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, targetGroupPermissions)
) {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem)
if (!usesValidKey) {
return this.buildFailResult(operation, ConflictType.GroupInvalidItemsKey, operation.existingItem)
}
return this.buildSuccessValue()
}
private async handleSaveToGroupOperation(operation: SaveToGroupSaveOperation): Promise<ItemSaveRuleResult> {
const groupPermissions = await this.getGroupPermissions(operation.userUuid, operation.groupUuid)
if (!groupPermissions) {
return this.buildFailResult(operation, ConflictType.GroupNotMemberError)
}
if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, groupPermissions)) {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (groupPermissions === 'read') {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem)
if (!usesValidKey) {
return this.buildFailResult(operation, ConflictType.GroupInvalidItemsKey, operation.existingItem)
}
return this.buildSuccessValue()
}
private async handleCreateToGroupOperation(operation: CreateToGroupSaveOperation): Promise<ItemSaveRuleResult> {
const groupPermissions = await this.getGroupPermissions(operation.userUuid, operation.groupUuid)
if (!groupPermissions) {
return this.buildFailResult(operation, ConflictType.GroupNotMemberError)
}
if (!this.isAuthorizedToSaveContentType(operation.incomingItem.content_type, groupPermissions)) {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
if (groupPermissions === 'read') {
return this.buildFailResult(operation, ConflictType.GroupInsufficientPermissionsError)
}
const usesValidKey = await this.incomingItemUsesValidItemsKey(operation.incomingItem)
if (!usesValidKey) {
return this.buildFailResult(operation, ConflictType.GroupInvalidItemsKey)
}
return this.buildSuccessValue()
}
private async incomingItemUsesValidItemsKey(itemHash: ItemHash): Promise<boolean> {
const isItemNotEncryptedByItemsKey = itemHash.content_type === ContentType.VaultItemsKey
if (isItemNotEncryptedByItemsKey) {
return true
}
const group = await this.groupService.getGroup({ groupUuid: itemHash.group_uuid as string })
const group = await this.groupService.getGroup({ groupUuid: itemHash.vault_system_identifier as string })
if (!group) {
return false
@@ -285,17 +94,4 @@ export class GroupFilter implements ItemSaveRuleInterface {
return itemHash.items_key_id === group.specifiedItemsKeyUuid
}
private async getGroupPermissions(userUuid: string, groupUuid: string): Promise<GroupUserPermission | undefined> {
const groupUser = await this.groupUserService.getUserForGroup({
userUuid,
groupUuid: groupUuid,
})
if (groupUser) {
return groupUser.permissions
}
return undefined
}
}
@@ -1,44 +0,0 @@
import { ItemHash } from '../ItemHash'
import { Item } from '../Item'
type SaveOperationCommon = {
incomingItem: ItemHash
userUuid: string
}
export type AddToGroupSaveOperation = SaveOperationCommon & {
type: 'add-to-group'
groupUuid: string
existingItem: Item
}
export type RemoveFromGroupSaveOperation = SaveOperationCommon & {
type: 'remove-from-group'
groupUuid: string
existingItem: Item
}
export type MoveToOtherGroupSaveOperation = SaveOperationCommon & {
type: 'move-to-other-group'
groupUuid: string
targetGroupUuid: string
existingItem: Item
}
export type SaveToGroupSaveOperation = SaveOperationCommon & {
type: 'save-to-group'
groupUuid: string
existingItem: Item
}
export type CreateToGroupSaveOperation = SaveOperationCommon & {
type: 'create-to-group'
groupUuid: string
}
export type GroupSaveOperation =
| AddToGroupSaveOperation
| RemoveFromGroupSaveOperation
| MoveToOtherGroupSaveOperation
| SaveToGroupSaveOperation
| CreateToGroupSaveOperation
@@ -12,7 +12,7 @@ export class GroupSnjsVersionFilter implements ItemSaveRuleInterface {
passed: true,
}
const groupUuidInvolved = dto.existingItem?.groupUuid || dto.itemHash.group_uuid
const groupUuidInvolved = dto.existingItem?.groupUuid || dto.itemHash.vault_system_identifier
if (!groupUuidInvolved) {
return successValue
}
@@ -5,7 +5,7 @@ import { ConflictType } from '../../../Tmp/ConflictType'
export class OwnershipFilter implements ItemSaveRuleInterface {
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
const deferToGroupFilter = dto.existingItem?.groupUuid || dto.itemHash.group_uuid
const deferToGroupFilter = dto.existingItem?.groupUuid
if (deferToGroupFilter) {
return {
passed: true,
@@ -1,35 +0,0 @@
import 'reflect-metadata'
import { Item } from '../../Item/Item'
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
import { GetGlobalItem } from './GetGlobalItem'
describe('GetGlobalItem', () => {
let itemRepository: ItemRepositoryInterface
const createUseCase = () => new GetGlobalItem(itemRepository)
beforeEach(() => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
})
it('should fail if item is not found', async () => {
expect(
await createUseCase().execute({
itemUuid: '2-3-4',
}),
).toEqual({ success: false, message: 'Could not find item with uuid 2-3-4' })
})
it('should succeed if item is found', async () => {
const item = {} as jest.Mocked<Item>
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
expect(
await createUseCase().execute({
itemUuid: '2-3-4',
}),
).toEqual({ success: true, item })
})
})
@@ -1,24 +0,0 @@
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { GetGlobalItemDTO } from './GetGlobalItemDTO'
import { GetGlobalItemResponse } from './GetGlobalItemResponse'
export class GetGlobalItem implements UseCaseInterface {
constructor(private itemRepository: ItemRepositoryInterface) {}
async execute(dto: GetGlobalItemDTO): Promise<GetGlobalItemResponse> {
const item = await this.itemRepository.findByUuid(dto.itemUuid)
if (item === null) {
return {
success: false,
message: `Could not find item with uuid ${dto.itemUuid}`,
}
}
return {
success: true,
item,
}
}
}
@@ -1,3 +0,0 @@
export type GetGlobalItemDTO = {
itemUuid: string
}
@@ -1,11 +0,0 @@
import { Item } from '../../Item/Item'
export type GetGlobalItemResponse =
| {
success: true
item: Item
}
| {
success: false
message: string
}
@@ -6,7 +6,7 @@ export type GroupInviteProjection = {
user_uuid: string
inviter_uuid: string
inviter_public_key: string
encrypted_group_data: string
encrypted_vault_key_content: string
invite_type: GroupInviteType
permissions: string
created_at_timestamp: number
@@ -23,7 +23,7 @@ export class GroupInviteProjector implements ProjectorInterface<GroupInvite, Gro
user_uuid: groupInvite.userUuid,
inviter_uuid: groupInvite.inviterUuid,
inviter_public_key: groupInvite.inviterPublicKey,
encrypted_group_data: groupInvite.encryptedGroupData,
encrypted_vault_key_content: groupInvite.encryptedVaultKeyContent,
invite_type: groupInvite.inviteType,
permissions: groupInvite.permissions,
created_at_timestamp: groupInvite.createdAtTimestamp,
@@ -5,9 +5,10 @@ export enum ConflictType {
ContentError = 'content_error',
ReadOnlyError = 'readonly_error',
UuidError = 'uuid_error',
SnjsVersionError = 'snjs_version_error',
GroupInsufficientPermissionsError = 'group_insufficient_permissions_error',
GroupNotMemberError = 'group_not_member_error',
GroupInvalidState = 'group_invalid_state',
GroupInvalidItemsKey = 'group_invalid_items_key',
}