mirror of
https://github.com/Art051/immich.git
synced 2025-08-11 19:29:00 +00:00
Modify Access repository, to evaluate `asset` permissions in bulk.
This is the last set of permission changes, to migrate all of them to
run in bulk!
Queries have been validated to match what they currently generate for single ids.
Queries:
* `activity` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "activity" "ActivityEntity"
WHERE
"ActivityEntity"."id" = $1
AND "ActivityEntity"."userId" = $2
)
LIMIT 1
-- After
SELECT "ActivityEntity"."id" AS "ActivityEntity_id"
FROM "activity" "ActivityEntity"
WHERE
"ActivityEntity"."id" IN ($1)
AND "ActivityEntity"."userId" = $2
```
* `activity` album owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
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" = $1
AND "ActivityEntity__ActivityEntity_album"."ownerId" = $2
)
LIMIT 1
-- After
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
```
* `activity` create access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
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" = $1
AND "AlbumEntity"."isActivityEnabled" = $2
AND "AlbumEntity__AlbumEntity_sharedUsers"."id" = $3
)
OR (
"AlbumEntity"."id" = $4
AND "AlbumEntity"."isActivityEnabled" = $5
AND "AlbumEntity"."ownerId" = $6
)
)
AND "AlbumEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
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"."isActivityEnabled" = $2
AND "AlbumEntity__AlbumEntity_sharedUsers"."id" = $3
)
OR (
"AlbumEntity"."id" IN ($4)
AND "AlbumEntity"."isActivityEnabled" = $5
AND "AlbumEntity"."ownerId" = $6
)
)
AND "AlbumEntity"."deletedAt" IS NULL
```
279 lines
9.9 KiB
TypeScript
279 lines
9.9 KiB
TypeScript
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
|
import { SharedLinkEntity } from '../../infra/entities';
|
|
import { AuthDto } from '../auth';
|
|
import { setDifference, setIsEqual, setUnion } from '../domain.util';
|
|
import { IAccessRepository } from '../repositories';
|
|
|
|
export enum Permission {
|
|
ACTIVITY_CREATE = 'activity.create',
|
|
ACTIVITY_DELETE = 'activity.delete',
|
|
|
|
// ASSET_CREATE = 'asset.create',
|
|
ASSET_READ = 'asset.read',
|
|
ASSET_UPDATE = 'asset.update',
|
|
ASSET_DELETE = 'asset.delete',
|
|
ASSET_RESTORE = 'asset.restore',
|
|
ASSET_SHARE = 'asset.share',
|
|
ASSET_VIEW = 'asset.view',
|
|
ASSET_DOWNLOAD = 'asset.download',
|
|
ASSET_UPLOAD = 'asset.upload',
|
|
|
|
// ALBUM_CREATE = 'album.create',
|
|
ALBUM_READ = 'album.read',
|
|
ALBUM_UPDATE = 'album.update',
|
|
ALBUM_DELETE = 'album.delete',
|
|
ALBUM_REMOVE_ASSET = 'album.removeAsset',
|
|
ALBUM_SHARE = 'album.share',
|
|
ALBUM_DOWNLOAD = 'album.download',
|
|
|
|
AUTH_DEVICE_DELETE = 'authDevice.delete',
|
|
|
|
ARCHIVE_READ = 'archive.read',
|
|
|
|
TIMELINE_READ = 'timeline.read',
|
|
TIMELINE_DOWNLOAD = 'timeline.download',
|
|
|
|
LIBRARY_CREATE = 'library.create',
|
|
LIBRARY_READ = 'library.read',
|
|
LIBRARY_UPDATE = 'library.update',
|
|
LIBRARY_DELETE = 'library.delete',
|
|
LIBRARY_DOWNLOAD = 'library.download',
|
|
|
|
PERSON_READ = 'person.read',
|
|
PERSON_WRITE = 'person.write',
|
|
PERSON_MERGE = 'person.merge',
|
|
PERSON_CREATE = 'person.create',
|
|
PERSON_REASSIGN = 'person.reassign',
|
|
|
|
PARTNER_UPDATE = 'partner.update',
|
|
}
|
|
|
|
let instance: AccessCore | null;
|
|
|
|
export class AccessCore {
|
|
private constructor(private repository: IAccessRepository) {}
|
|
|
|
static create(repository: IAccessRepository) {
|
|
if (!instance) {
|
|
instance = new AccessCore(repository);
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
static reset() {
|
|
instance = null;
|
|
}
|
|
|
|
requireUploadAccess(auth: AuthDto | null): AuthDto {
|
|
if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
|
|
throw new UnauthorizedException();
|
|
}
|
|
return auth;
|
|
}
|
|
|
|
/**
|
|
* Check if user has access to all ids, for the given permission.
|
|
* Throws error if user does not have access to any of the ids.
|
|
*/
|
|
async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) {
|
|
ids = Array.isArray(ids) ? ids : [ids];
|
|
const allowedIds = await this.checkAccess(auth, permission, ids);
|
|
if (!setIsEqual(new Set(ids), allowedIds)) {
|
|
throw new BadRequestException(`Not found or no ${permission} access`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return ids that user has access to, for the given permission.
|
|
* Check is done for each id, and only allowed ids are returned.
|
|
*
|
|
* @returns Set<string>
|
|
*/
|
|
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]) {
|
|
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
|
if (idSet.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
if (auth.sharedLink) {
|
|
return this.checkAccessSharedLink(auth.sharedLink, permission, idSet);
|
|
}
|
|
|
|
return this.checkAccessOther(auth, permission, idSet);
|
|
}
|
|
|
|
private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set<string>) {
|
|
const sharedLinkId = sharedLink.id;
|
|
|
|
switch (permission) {
|
|
case Permission.ASSET_READ:
|
|
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
|
|
|
case Permission.ASSET_VIEW:
|
|
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
|
|
|
case Permission.ASSET_DOWNLOAD:
|
|
return !!sharedLink.allowDownload
|
|
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
|
|
: new Set();
|
|
|
|
case Permission.ASSET_UPLOAD:
|
|
return sharedLink.allowUpload ? ids : new Set();
|
|
|
|
case Permission.ASSET_SHARE:
|
|
// TODO: fix this to not use sharedLink.userId for access control
|
|
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
|
|
|
|
case Permission.ALBUM_READ:
|
|
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
|
|
|
|
case Permission.ALBUM_DOWNLOAD:
|
|
return !!sharedLink.allowDownload
|
|
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
|
|
: new Set();
|
|
|
|
default:
|
|
return new Set();
|
|
}
|
|
}
|
|
|
|
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
|
|
switch (permission) {
|
|
// uses album id
|
|
case Permission.ACTIVITY_CREATE:
|
|
return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
|
|
|
|
// uses activity id
|
|
case Permission.ACTIVITY_DELETE: {
|
|
const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids);
|
|
const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess(
|
|
auth.user.id,
|
|
setDifference(ids, isOwner),
|
|
);
|
|
return setUnion(isOwner, isAlbumOwner);
|
|
}
|
|
|
|
case Permission.ASSET_READ: {
|
|
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
|
const isPartner = await this.repository.asset.checkPartnerAccess(
|
|
auth.user.id,
|
|
setDifference(ids, isOwner, isAlbum),
|
|
);
|
|
return setUnion(isOwner, isAlbum, isPartner);
|
|
}
|
|
|
|
case Permission.ASSET_SHARE: {
|
|
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
|
return setUnion(isOwner, isPartner);
|
|
}
|
|
|
|
case Permission.ASSET_VIEW: {
|
|
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
|
const isPartner = await this.repository.asset.checkPartnerAccess(
|
|
auth.user.id,
|
|
setDifference(ids, isOwner, isAlbum),
|
|
);
|
|
return setUnion(isOwner, isAlbum, isPartner);
|
|
}
|
|
|
|
case Permission.ASSET_DOWNLOAD: {
|
|
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
|
const isPartner = await this.repository.asset.checkPartnerAccess(
|
|
auth.user.id,
|
|
setDifference(ids, isOwner, isAlbum),
|
|
);
|
|
return setUnion(isOwner, isAlbum, isPartner);
|
|
}
|
|
|
|
case Permission.ASSET_UPDATE:
|
|
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ASSET_DELETE:
|
|
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ASSET_RESTORE:
|
|
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ALBUM_READ: {
|
|
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
|
return setUnion(isOwner, isShared);
|
|
}
|
|
|
|
case Permission.ALBUM_UPDATE:
|
|
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ALBUM_DELETE:
|
|
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ALBUM_SHARE:
|
|
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ALBUM_DOWNLOAD: {
|
|
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
|
return setUnion(isOwner, isShared);
|
|
}
|
|
|
|
case Permission.ALBUM_REMOVE_ASSET:
|
|
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ASSET_UPLOAD:
|
|
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.ARCHIVE_READ:
|
|
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
|
|
|
case Permission.AUTH_DEVICE_DELETE:
|
|
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.TIMELINE_READ: {
|
|
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
|
const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
|
return setUnion(isOwner, isPartner);
|
|
}
|
|
|
|
case Permission.TIMELINE_DOWNLOAD:
|
|
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
|
|
|
case Permission.LIBRARY_READ: {
|
|
const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
|
return setUnion(isOwner, isPartner);
|
|
}
|
|
|
|
case Permission.LIBRARY_UPDATE:
|
|
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.LIBRARY_DELETE:
|
|
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PERSON_READ:
|
|
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PERSON_WRITE:
|
|
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PERSON_MERGE:
|
|
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PERSON_CREATE:
|
|
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PERSON_REASSIGN:
|
|
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
|
|
|
case Permission.PARTNER_UPDATE:
|
|
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
|
|
|
|
default:
|
|
return new Set();
|
|
}
|
|
}
|
|
}
|