From 7fc4abba72ebe0b157c52c762a41c74cfbd68cc7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 25 Jan 2024 10:14:38 -0500 Subject: [PATCH] feat(server): sql access checks (#6635) --- server/src/infra/infra.util.ts | 7 +- .../infra/repositories/access.repository.ts | 900 ++++++++++-------- server/src/infra/sql-generator/index.ts | 29 +- server/src/infra/sql/access.repository.sql | 242 +++++ 4 files changed, 760 insertions(+), 418 deletions(-) diff --git a/server/src/infra/infra.util.ts b/server/src/infra/infra.util.ts index 8dcf6bf1ac..4dc821cd57 100644 --- a/server/src/infra/infra.util.ts +++ b/server/src/infra/infra.util.ts @@ -4,14 +4,17 @@ export const GENERATE_SQL_KEY = 'generate-sql-key'; export interface GenerateSqlQueries { name?: string; - params?: any[]; + params: unknown[]; } /** Decorator to enable versioning/tracking of generated Sql */ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); +const UUID = '00000000-0000-4000-a000-000000000000'; + export const DummyValue = { - UUID: '00000000-0000-4000-a000-000000000000', + UUID, + UUID_SET: new Set([UUID]), PAGINATION: { take: 10, skip: 0 }, EMAIL: 'user@immich.app', STRING: 'abcdefghi', diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 359dca3943..f275b51713 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -1,7 +1,6 @@ -import { IAccessRepository } from '@app/domain'; +import { IAccessRepository, chunks, setUnion } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, In, Repository } from 'typeorm'; -import { chunks, setUnion } from '../../domain/domain.util'; import { ActivityEntity, AlbumEntity, @@ -13,426 +12,503 @@ import { SharedLinkEntity, UserTokenEntity, } from '../entities'; -import { DATABASE_PARAMETER_CHUNK_SIZE } from '../infra.util'; +import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util'; -export class AccessRepository implements IAccessRepository { +type IActivityAccess = IAccessRepository['activity']; +type IAlbumAccess = IAccessRepository['album']; +type IAssetAccess = IAccessRepository['asset']; +type IAuthDeviceAccess = IAccessRepository['authDevice']; +type ILibraryAccess = IAccessRepository['library']; +type ITimelineAccess = IAccessRepository['timeline']; +type IPersonAccess = IAccessRepository['person']; +type IPartnerAccess = IAccessRepository['partner']; + +class ActivityAccess implements IActivityAccess { constructor( - @InjectRepository(ActivityEntity) private activityRepository: Repository, - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(AlbumEntity) private albumRepository: Repository, - @InjectRepository(LibraryEntity) private libraryRepository: Repository, - @InjectRepository(PartnerEntity) private partnerRepository: Repository, - @InjectRepository(PersonEntity) private personRepository: Repository, - @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, - @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository, - @InjectRepository(UserTokenEntity) private tokenRepository: Repository, + private activityRepository: Repository, + private albumRepository: Repository, ) {} - activity = { - checkOwnerAccess: async (userId: string, activityIds: Set): Promise> => { - if (activityIds.size === 0) { - return new Set(); - } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, activityIds: Set): Promise> { + if (activityIds.size === 0) { + return new Set(); + } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - userId, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); - }, + return Promise.all( + chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.activityRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + userId, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))), + ), + ).then((results) => setUnion(...results)); + } - checkAlbumOwnerAccess: async (userId: string, activityIds: Set): Promise> => { - if (activityIds.size === 0) { - return new Set(); - } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkAlbumOwnerAccess(userId: string, activityIds: Set): Promise> { + if (activityIds.size === 0) { + return new Set(); + } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - album: { - ownerId: userId, - }, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); - }, - - checkCreateAccess: async (userId: string, albumIds: Set): Promise> => { - if (albumIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .select('album.id') - .leftJoin('album.sharedUsers', 'sharedUsers') - .where('album.id IN (:...albumIds)', { albumIds: idChunk }) - .andWhere('album.isActivityEnabled = true') - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getMany() - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); - }, - }; - - library = { - checkOwnerAccess: async (userId: string, libraryIds: Set): Promise> => { - if (libraryIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.libraryRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), + return Promise.all( + chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.activityRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + album: { ownerId: userId, }, - }) - .then((libraries) => new Set(libraries.map((library) => library.id))), - ), - ).then((results) => setUnion(...results)); - }, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))), + ), + ).then((results) => setUnion(...results)); + } - checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { - if (partnerIds.size === 0) { - return new Set(); - } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkCreateAccess(userId: string, albumIds: Set): Promise> { + if (albumIds.size === 0) { + return new Set(); + } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); - }, - }; - - timeline = { - checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { - if (partnerIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); - }, - }; - - asset = { - checkAlbumAccess: async (userId: string, assetIds: Set): Promise> => { - if (assetIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .innerJoin('album.assets', 'asset') - .leftJoin('album.sharedUsers', 'sharedUsers') - .select('asset.id', 'assetId') - .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') - .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { - assetIds: idChunk, - }) - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); - } - if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { - allowedIds.add(row.livePhotoVideoId); - } - } - return allowedIds; + return Promise.all( + chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.albumRepository + .createQueryBuilder('album') + .select('album.id') + .leftJoin('album.sharedUsers', 'sharedUsers') + .where('album.id IN (:...albumIds)', { albumIds: idChunk }) + .andWhere('album.isActivityEnabled = true') + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); }), - ), - ).then((results) => setUnion(...results)); - }, - - checkOwnerAccess: async (userId: string, assetIds: Set): Promise> => { - if (assetIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - withDeleted: true, - }) - .then((assets) => new Set(assets.map((asset) => asset.id))), - ), - ).then((results) => setUnion(...results)); - }, - - checkPartnerAccess: async (userId: string, assetIds: Set): Promise> => { - if (assetIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .innerJoin('partner.sharedBy', 'sharedBy') - .innerJoin('sharedBy.assets', 'asset') - .select('asset.id', 'assetId') - .where('partner.sharedWithId = :userId', { userId }) - .andWhere('asset.id IN (:...assetIds)', { assetIds: idChunk }) - .getRawMany() - .then((rows) => new Set(rows.map((row) => row.assetId))), - ), - ).then((results) => setUnion(...results)); - }, - - checkSharedLinkAccess: async (sharedLinkId: string, assetIds: Set): Promise> => { - if (assetIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.sharedLinkRepository - .createQueryBuilder('sharedLink') - .leftJoin('sharedLink.album', 'album') - .leftJoin('sharedLink.assets', 'assets') - .leftJoin('album.assets', 'albumAssets') - .select('assets.id', 'assetId') - .addSelect('albumAssets.id', 'albumAssetId') - .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') - .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') - .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) - .andWhere( - 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', - { - assetIds: idChunk, - }, - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); - } - if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { - allowedIds.add(row.assetLivePhotoVideoId); - } - if (row.albumAssetId && assetIds.has(row.albumAssetId)) { - allowedIds.add(row.albumAssetId); - } - if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { - allowedIds.add(row.albumAssetLivePhotoVideoId); - } - } - return allowedIds; - }), - ), - ).then((results) => setUnion(...results)); - }, - }; - - authDevice = { - checkOwnerAccess: async (userId: string, deviceIds: Set): Promise> => { - if (deviceIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.tokenRepository - .find({ - select: { id: true }, - where: { - userId, - id: In(idChunk), - }, - }) - .then((tokens) => new Set(tokens.map((token) => token.id))), - ), - ).then((results) => setUnion(...results)); - }, - }; - - album = { - checkOwnerAccess: async (userId: string, albumIds: Set): Promise> => { - if (albumIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); - }, - - checkSharedAlbumAccess: async (userId: string, albumIds: Set): Promise> => { - if (albumIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - sharedUsers: { - id: userId, - }, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); - }, - - checkSharedLinkAccess: async (sharedLinkId: string, albumIds: Set): Promise> => { - if (albumIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.sharedLinkRepository - .find({ - select: { albumId: true }, - where: { - id: sharedLinkId, - albumId: In(idChunk), - }, - }) - .then( - (sharedLinks) => - new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))), - ), - ), - ).then((results) => setUnion(...results)); - }, - }; - - person = { - checkOwnerAccess: async (userId: string, personIds: Set): Promise> => { - if (personIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.personRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((persons) => new Set(persons.map((person) => person.id))), - ), - ).then((results) => setUnion(...results)); - }, - - checkFaceOwnerAccess: async (userId: string, assetFaceIds: Set): Promise> => { - if (assetFaceIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetFaceRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - asset: { - ownerId: userId, - }, - }, - }) - .then((faces) => new Set(faces.map((face) => face.id))), - ), - ).then((results) => setUnion(...results)); - }, - }; - - partner = { - checkUpdateAccess: async (userId: string, partnerIds: Set): Promise> => { - if (partnerIds.size === 0) { - return new Set(); - } - - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); - }, - }; + ) + .getMany() + .then((albums) => new Set(albums.map((album) => album.id))), + ), + ).then((results) => setUnion(...results)); + } +} + +class AlbumAccess implements IAlbumAccess { + constructor( + private albumRepository: Repository, + private sharedLinkRepository: Repository, + ) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, albumIds: Set): Promise> { + if (albumIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.albumRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + ownerId: userId, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkSharedAlbumAccess(userId: string, albumIds: Set): Promise> { + if (albumIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.albumRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + sharedUsers: { + id: userId, + }, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise> { + if (albumIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.sharedLinkRepository + .find({ + select: { albumId: true }, + where: { + id: sharedLinkId, + albumId: In(idChunk), + }, + }) + .then( + (sharedLinks) => + new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))), + ), + ), + ).then((results) => setUnion(...results)); + } +} + +class AssetAccess implements IAssetAccess { + constructor( + private albumRepository: Repository, + private assetRepository: Repository, + private partnerRepository: Repository, + private sharedLinkRepository: Repository, + ) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkAlbumAccess(userId: string, assetIds: Set): Promise> { + if (assetIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.albumRepository + .createQueryBuilder('album') + .innerJoin('album.assets', 'asset') + .leftJoin('album.sharedUsers', 'sharedUsers') + .select('asset.id', 'assetId') + .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') + .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { + assetIds: idChunk, + }) + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { + allowedIds.add(row.livePhotoVideoId); + } + } + return allowedIds; + }), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, assetIds: Set): Promise> { + if (assetIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.assetRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + ownerId: userId, + }, + withDeleted: true, + }) + .then((assets) => new Set(assets.map((asset) => asset.id))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkPartnerAccess(userId: string, assetIds: Set): Promise> { + if (assetIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.partnerRepository + .createQueryBuilder('partner') + .innerJoin('partner.sharedBy', 'sharedBy') + .innerJoin('sharedBy.assets', 'asset') + .select('asset.id', 'assetId') + .where('partner.sharedWithId = :userId', { userId }) + .andWhere('asset.id IN (:...assetIds)', { assetIds: idChunk }) + .getRawMany() + .then((rows) => new Set(rows.map((row) => row.assetId))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise> { + if (assetIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.sharedLinkRepository + .createQueryBuilder('sharedLink') + .leftJoin('sharedLink.album', 'album') + .leftJoin('sharedLink.assets', 'assets') + .leftJoin('album.assets', 'albumAssets') + .select('assets.id', 'assetId') + .addSelect('albumAssets.id', 'albumAssetId') + .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') + .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') + .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) + .andWhere( + 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', + { + assetIds: idChunk, + }, + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { + allowedIds.add(row.assetLivePhotoVideoId); + } + if (row.albumAssetId && assetIds.has(row.albumAssetId)) { + allowedIds.add(row.albumAssetId); + } + if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { + allowedIds.add(row.albumAssetLivePhotoVideoId); + } + } + return allowedIds; + }), + ), + ).then((results) => setUnion(...results)); + } +} + +class AuthDeviceAccess implements IAuthDeviceAccess { + constructor(private tokenRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, deviceIds: Set): Promise> { + if (deviceIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In(idChunk), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))), + ), + ).then((results) => setUnion(...results)); + } +} + +class LibraryAccess implements ILibraryAccess { + constructor( + private libraryRepository: Repository, + private partnerRepository: Repository, + ) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, libraryIds: Set): Promise> { + if (libraryIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { + if (partnerIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))), + ), + ).then((results) => setUnion(...results)); + } +} + +class TimelineAccess implements ITimelineAccess { + constructor(private partnerRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { + if (partnerIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))), + ), + ).then((results) => setUnion(...results)); + } +} + +class PersonAccess implements IPersonAccess { + constructor( + private assetFaceRepository: Repository, + private personRepository: Repository, + ) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkOwnerAccess(userId: string, personIds: Set): Promise> { + if (personIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.personRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))), + ), + ).then((results) => setUnion(...results)); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkFaceOwnerAccess(userId: string, assetFaceIds: Set): Promise> { + if (assetFaceIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.assetFaceRepository + .find({ + select: { id: true }, + where: { + id: In(idChunk), + asset: { + ownerId: userId, + }, + }, + }) + .then((faces) => new Set(faces.map((face) => face.id))), + ), + ).then((results) => setUnion(...results)); + } +} + +class PartnerAccess implements IPartnerAccess { + constructor(private partnerRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + async checkUpdateAccess(userId: string, partnerIds: Set): Promise> { + if (partnerIds.size === 0) { + return new Set(); + } + + return Promise.all( + chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => + this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))), + ), + ).then((results) => setUnion(...results)); + } +} + +export class AccessRepository implements IAccessRepository { + activity: IActivityAccess; + album: IAlbumAccess; + asset: IAssetAccess; + authDevice: IAuthDeviceAccess; + library: ILibraryAccess; + person: IPersonAccess; + partner: IPartnerAccess; + timeline: ITimelineAccess; + + constructor( + @InjectRepository(ActivityEntity) activityRepository: Repository, + @InjectRepository(AssetEntity) assetRepository: Repository, + @InjectRepository(AlbumEntity) albumRepository: Repository, + @InjectRepository(LibraryEntity) libraryRepository: Repository, + @InjectRepository(PartnerEntity) partnerRepository: Repository, + @InjectRepository(PersonEntity) personRepository: Repository, + @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, + @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, + @InjectRepository(UserTokenEntity) tokenRepository: Repository, + ) { + this.activity = new ActivityAccess(activityRepository, albumRepository); + this.album = new AlbumAccess(albumRepository, sharedLinkRepository); + this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); + this.authDevice = new AuthDeviceAccess(tokenRepository); + this.library = new LibraryAccess(libraryRepository, partnerRepository); + this.person = new PersonAccess(assetFaceRepository, personRepository); + this.partner = new PartnerAccess(partnerRepository); + this.timeline = new TimelineAccess(partnerRepository); + } } diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts index 761deafc41..b4b0978a23 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/infra/sql-generator/index.ts @@ -98,8 +98,25 @@ class SqlGenerator { const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`]; const instance = this.app.get(Repository); - const properties = Object.getOwnPropertyNames(Repository.prototype) as Array; - for (const key of properties) { + + // normal repositories + data.push(...(await this.runTargets(instance, `${Repository.name}`))); + + // nested repositories + if (Repository.name === AccessRepository.name) { + for (const key of Object.keys(instance)) { + const subInstance = (instance as any)[key]; + data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`))); + } + } + + this.results[Repository.name] = data; + } + + private async runTargets(instance: any, label: string) { + const data: string[] = []; + + for (const key of this.getPropertyNames(instance)) { const target = instance[key]; if (!(target instanceof Function)) { continue; @@ -116,7 +133,7 @@ class SqlGenerator { } for (const { name, params } of queries) { - let queryLabel = `${Repository.name}.${key}`; + let queryLabel = `${label}.${key}`; if (name) { queryLabel += ` (${name})`; } @@ -135,7 +152,7 @@ class SqlGenerator { } } - this.results[Repository.name] = data; + return data; } private async write() { @@ -156,6 +173,10 @@ class SqlGenerator { await this.app.close(); } } + + private getPropertyNames(instance: any): string[] { + return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) as any[]; + } } new SqlGenerator({ targetDir: './src/infra/sql' }) diff --git a/server/src/infra/sql/access.repository.sql b/server/src/infra/sql/access.repository.sql index 21f9f116bd..f2ed3f9a38 100644 --- a/server/src/infra/sql/access.repository.sql +++ b/server/src/infra/sql/access.repository.sql @@ -1 +1,243 @@ -- NOTE: This file is auto generated by ./sql-generator + +-- AccessRepository.activity.checkOwnerAccess +SELECT + "ActivityEntity"."id" AS "ActivityEntity_id" +FROM + "activity" "ActivityEntity" +WHERE + ( + "ActivityEntity"."id" IN ($1) + AND "ActivityEntity"."userId" = $2 + ) + +-- AccessRepository.activity.checkAlbumOwnerAccess +SELECT + "ActivityEntity"."id" AS "ActivityEntity_id" +FROM + "activity" "ActivityEntity" + LEFT JOIN "albums" "ActivityEntity__ActivityEntity_album" ON "ActivityEntity__ActivityEntity_album"."id" = "ActivityEntity"."albumId" + AND ( + "ActivityEntity__ActivityEntity_album"."deletedAt" IS NULL + ) +WHERE + ( + "ActivityEntity"."id" IN ($1) + AND "ActivityEntity__ActivityEntity_album"."ownerId" = $2 + ) + +-- AccessRepository.activity.checkCreateAccess +SELECT + "album"."id" AS "album_id" +FROM + "albums" "album" + LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id" + LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId" + AND ("sharedUsers"."deletedAt" IS NULL) +WHERE + ( + "album"."id" IN ($1) + AND "album"."isActivityEnabled" = true + AND ( + "album"."ownerId" = $2 + OR "sharedUsers"."id" = $2 + ) + ) + AND ("album"."deletedAt" IS NULL) + +-- AccessRepository.album.checkOwnerAccess +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id" +FROM + "albums" "AlbumEntity" +WHERE + ( + ( + "AlbumEntity"."id" IN ($1) + AND "AlbumEntity"."ownerId" = $2 + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) + +-- AccessRepository.album.checkSharedAlbumAccess +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id" +FROM + "albums" "AlbumEntity" + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) +WHERE + ( + ( + "AlbumEntity"."id" IN ($1) + AND "AlbumEntity__AlbumEntity_sharedUsers"."id" = $2 + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) + +-- AccessRepository.album.checkSharedLinkAccess +SELECT + "SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId", + "SharedLinkEntity"."id" AS "SharedLinkEntity_id" +FROM + "shared_links" "SharedLinkEntity" +WHERE + ( + "SharedLinkEntity"."id" = $1 + AND "SharedLinkEntity"."albumId" IN ($2) + ) + +-- AccessRepository.asset.checkAlbumAccess +SELECT + "asset"."id" AS "assetId", + "asset"."livePhotoVideoId" AS "livePhotoVideoId" +FROM + "albums" "album" + INNER JOIN "albums_assets_assets" "album_asset" ON "album_asset"."albumsId" = "album"."id" + INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId" + AND ("asset"."deletedAt" IS NULL) + LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id" + LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId" + AND ("sharedUsers"."deletedAt" IS NULL) +WHERE + ( + array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid [] + AND ( + "album"."ownerId" = $2 + OR "sharedUsers"."id" = $2 + ) + ) + AND ("album"."deletedAt" IS NULL) + +-- AccessRepository.asset.checkOwnerAccess +SELECT + "AssetEntity"."id" AS "AssetEntity_id" +FROM + "assets" "AssetEntity" +WHERE + ( + "AssetEntity"."id" IN ($1) + AND "AssetEntity"."ownerId" = $2 + ) + +-- AccessRepository.asset.checkPartnerAccess +SELECT + "asset"."id" AS "assetId" +FROM + "partners" "partner" + INNER JOIN "users" "sharedBy" ON "sharedBy"."id" = "partner"."sharedById" + AND ("sharedBy"."deletedAt" IS NULL) + INNER JOIN "assets" "asset" ON "asset"."ownerId" = "sharedBy"."id" + AND ("asset"."deletedAt" IS NULL) +WHERE + "partner"."sharedWithId" = $1 + AND "asset"."id" IN ($2) + +-- AccessRepository.asset.checkSharedLinkAccess +SELECT + "assets"."id" AS "assetId", + "assets"."livePhotoVideoId" AS "assetLivePhotoVideoId", + "albumAssets"."id" AS "albumAssetId", + "albumAssets"."livePhotoVideoId" AS "albumAssetLivePhotoVideoId" +FROM + "shared_links" "sharedLink" + LEFT JOIN "albums" "album" ON "album"."id" = "sharedLink"."albumId" + AND ("album"."deletedAt" IS NULL) + LEFT JOIN "shared_link__asset" "assets_sharedLink" ON "assets_sharedLink"."sharedLinksId" = "sharedLink"."id" + LEFT JOIN "assets" "assets" ON "assets"."id" = "assets_sharedLink"."assetsId" + AND ("assets"."deletedAt" IS NULL) + LEFT JOIN "albums_assets_assets" "album_albumAssets" ON "album_albumAssets"."albumsId" = "album"."id" + LEFT JOIN "assets" "albumAssets" ON "albumAssets"."id" = "album_albumAssets"."assetsId" + AND ("albumAssets"."deletedAt" IS NULL) +WHERE + "sharedLink"."id" = $1 + AND array[ + "assets"."id", + "assets"."livePhotoVideoId", + "albumAssets"."id", + "albumAssets"."livePhotoVideoId" + ] && array[$2]::uuid [] + +-- AccessRepository.authDevice.checkOwnerAccess +SELECT + "UserTokenEntity"."id" AS "UserTokenEntity_id" +FROM + "user_token" "UserTokenEntity" +WHERE + ( + "UserTokenEntity"."userId" = $1 + AND "UserTokenEntity"."id" IN ($2) + ) + +-- AccessRepository.library.checkOwnerAccess +SELECT + "LibraryEntity"."id" AS "LibraryEntity_id" +FROM + "libraries" "LibraryEntity" +WHERE + ( + ( + "LibraryEntity"."id" IN ($1) + AND "LibraryEntity"."ownerId" = $2 + ) + ) + AND ("LibraryEntity"."deletedAt" IS NULL) + +-- AccessRepository.library.checkPartnerAccess +SELECT + "partner"."sharedById" AS "partner_sharedById", + "partner"."sharedWithId" AS "partner_sharedWithId" +FROM + "partners" "partner" +WHERE + "partner"."sharedById" IN ($1) + AND "partner"."sharedWithId" = $2 + +-- AccessRepository.person.checkOwnerAccess +SELECT + "PersonEntity"."id" AS "PersonEntity_id" +FROM + "person" "PersonEntity" +WHERE + ( + "PersonEntity"."id" IN ($1) + AND "PersonEntity"."ownerId" = $2 + ) + +-- AccessRepository.person.checkFaceOwnerAccess +SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id" +FROM + "asset_faces" "AssetFaceEntity" + LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId" + AND ( + "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL + ) +WHERE + ( + "AssetFaceEntity"."id" IN ($1) + AND "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" = $2 + ) + +-- AccessRepository.partner.checkUpdateAccess +SELECT + "partner"."sharedById" AS "partner_sharedById", + "partner"."sharedWithId" AS "partner_sharedWithId" +FROM + "partners" "partner" +WHERE + "partner"."sharedById" IN ($1) + AND "partner"."sharedWithId" = $2 + +-- AccessRepository.timeline.checkPartnerAccess +SELECT + "partner"."sharedById" AS "partner_sharedById", + "partner"."sharedWithId" AS "partner_sharedWithId" +FROM + "partners" "partner" +WHERE + "partner"."sharedById" IN ($1) + AND "partner"."sharedWithId" = $2