mirror of
https://github.com/immich-app/immich.git
synced 2026-02-15 13:28:24 +03:00
chore(server): Check asset permissions in bulk (#5329)
Modify Access repository, to evaluate `asset` permissions in bulk.
Queries have been validated to match what they currently generate for single ids.
Queries:
* `asset` album 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_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets"
ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId"="AlbumEntity"."id"
LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets"
ON "AlbumEntity__AlbumEntity_assets"."id"="AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId"
AND "AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL
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"."ownerId" = $1 AND "AlbumEntity__AlbumEntity_assets"."id" = $2)
OR ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $3 AND "AlbumEntity__AlbumEntity_assets"."id" = $4)
OR ("AlbumEntity"."ownerId" = $5 AND "AlbumEntity__AlbumEntity_assets"."livePhotoVideoId" = $6)
OR ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $7 AND "AlbumEntity__AlbumEntity_assets"."livePhotoVideoId" = $8)
)
AND "AlbumEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
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
(
"album"."ownerId" = $1
OR "sharedUsers"."id" = $2
)
AND (
"asset"."id" IN ($3, $4)
OR "asset"."livePhotoVideoId" IN ($5, $6)
)
AND "album"."deletedAt" IS NULL
```
* `asset` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "assets" "AssetEntity"
WHERE
"AssetEntity"."id" = $1
AND "AssetEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT
"AssetEntity"."id" AS "AssetEntity_id"
FROM "assets" "AssetEntity"
WHERE
"AssetEntity"."id" IN ($1, $2)
AND "AssetEntity"."ownerId" = $3
```
* `asset` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__PartnerEntity_sharedWith"
ON "PartnerEntity__PartnerEntity_sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__PartnerEntity_sharedWith"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__PartnerEntity_sharedBy"
ON "PartnerEntity__PartnerEntity_sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__PartnerEntity_sharedBy"."deletedAt" IS NULL
LEFT JOIN "assets" "0aabe9f4a62b794e2c24a074297e534f51a4ac6c"
ON "0aabe9f4a62b794e2c24a074297e534f51a4ac6c"."ownerId"="PartnerEntity__PartnerEntity_sharedBy"."id"
AND "0aabe9f4a62b794e2c24a074297e534f51a4ac6c"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity__PartnerEntity_sharedWith"."id" = $1
AND "0aabe9f4a62b794e2c24a074297e534f51a4ac6c"."id" = $2
)
LIMIT 1
-- After
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, $3)
```
* `asset` shared link access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "shared_links" "SharedLinkEntity"
LEFT JOIN "albums" "SharedLinkEntity__SharedLinkEntity_album"
ON "SharedLinkEntity__SharedLinkEntity_album"."id"="SharedLinkEntity"."albumId"
AND "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" IS NULL
LEFT JOIN "albums_assets_assets" "760f12c00d97bdcec1ce224d1e3bf449859942b6"
ON "760f12c00d97bdcec1ce224d1e3bf449859942b6"."albumsId"="SharedLinkEntity__SharedLinkEntity_album"."id"
LEFT JOIN "assets" "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"
ON "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id"="760f12c00d97bdcec1ce224d1e3bf449859942b6"."assetsId"
AND "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deletedAt" IS NULL
LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"
ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId"="SharedLinkEntity"."id"
LEFT JOIN "assets" "SharedLinkEntity__SharedLinkEntity_assets"
ON "SharedLinkEntity__SharedLinkEntity_assets"."id"="SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."assetsId"
AND "SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" IS NULL
WHERE (
("SharedLinkEntity"."id" = $1 AND "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" = $2)
OR ("SharedLinkEntity"."id" = $3 AND "SharedLinkEntity__SharedLinkEntity_assets"."id" = $4)
OR ("SharedLinkEntity"."id" = $5 AND "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."livePhotoVideoId" = $6)
OR ("SharedLinkEntity"."id" = $7 AND "SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" = $8)
)
)
LIMIT 1
-- After
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 (
"assets"."id" IN ($2, $3)
OR "albumAssets"."id" IN ($4, $5)
OR "assets"."livePhotoVideoId" IN ($6, $7)
OR "albumAssets"."livePhotoVideoId" IN ($8, $9)
)
```
This commit is contained in:
committed by
GitHub
parent
6673f1eb24
commit
5aa658de59
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { setDifference, setUnion } from '../domain.util';
|
||||
import { setDifference, setIsEqual, setUnion } from '../domain.util';
|
||||
import { IAccessRepository } from '../repositories';
|
||||
|
||||
export enum Permission {
|
||||
@@ -76,7 +76,7 @@ export class AccessCore {
|
||||
async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
||||
ids = Array.isArray(ids) ? ids : [ids];
|
||||
const allowedIds = await this.checkAccess(authUser, permission, ids);
|
||||
if (new Set(ids).size !== allowedIds.size) {
|
||||
if (!setIsEqual(new Set(ids), allowedIds)) {
|
||||
throw new BadRequestException(`Not found or no ${permission} access`);
|
||||
}
|
||||
}
|
||||
@@ -106,9 +106,24 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
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 !!authUser.isAllowDownload
|
||||
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
|
||||
case Permission.ASSET_UPLOAD:
|
||||
return authUser.isAllowUpload ? ids : new Set();
|
||||
|
||||
case Permission.ASSET_SHARE:
|
||||
// TODO: fix this to not use authUser.id for shared link access control
|
||||
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ALBUM_READ:
|
||||
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
|
||||
@@ -116,46 +131,59 @@ export class AccessCore {
|
||||
return !!authUser.isAllowDownload
|
||||
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
const allowedIds = new Set();
|
||||
for (const id of ids) {
|
||||
const hasAccess = await this.hasSharedLinkAccess(authUser, permission, id);
|
||||
if (hasAccess) {
|
||||
allowedIds.add(id);
|
||||
}
|
||||
}
|
||||
return allowedIds;
|
||||
}
|
||||
|
||||
// TODO: Migrate logic to checkAccessSharedLink to evaluate permissions in bulk.
|
||||
private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) {
|
||||
const sharedLinkId = authUser.sharedLinkId;
|
||||
if (!sharedLinkId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (permission) {
|
||||
case Permission.ASSET_READ:
|
||||
return this.repository.asset.hasSharedLinkAccess(sharedLinkId, id);
|
||||
|
||||
case Permission.ASSET_VIEW:
|
||||
return await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id);
|
||||
|
||||
case Permission.ASSET_DOWNLOAD:
|
||||
return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id));
|
||||
|
||||
case Permission.ASSET_SHARE:
|
||||
// TODO: fix this to not use authUser.id for shared link access control
|
||||
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
||||
|
||||
default:
|
||||
return false;
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
|
||||
switch (permission) {
|
||||
case Permission.ASSET_READ: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
authUser.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
authUser.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
|
||||
const isPartner = await this.repository.asset.checkPartnerAccess(
|
||||
authUser.id,
|
||||
setDifference(ids, isOwner, isAlbum),
|
||||
);
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPDATE:
|
||||
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ASSET_DELETE:
|
||||
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ASSET_RESTORE:
|
||||
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
|
||||
@@ -163,13 +191,13 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
case Permission.ALBUM_UPDATE:
|
||||
return this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ALBUM_DELETE:
|
||||
return this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ALBUM_SHARE:
|
||||
return this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
@@ -178,16 +206,16 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
case Permission.ALBUM_REMOVE_ASSET:
|
||||
return this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ASSET_UPLOAD:
|
||||
return this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.ARCHIVE_READ:
|
||||
return ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
|
||||
|
||||
case Permission.AUTH_DEVICE_DELETE:
|
||||
return this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.TIMELINE_READ: {
|
||||
const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set<string>();
|
||||
@@ -205,22 +233,22 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
case Permission.LIBRARY_UPDATE:
|
||||
return this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.LIBRARY_DELETE:
|
||||
return this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_READ:
|
||||
return this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_WRITE:
|
||||
return this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_MERGE:
|
||||
return this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PARTNER_UPDATE:
|
||||
return this.repository.partner.checkUpdateAccess(authUser.id, ids);
|
||||
return await this.repository.partner.checkUpdateAccess(authUser.id, ids);
|
||||
}
|
||||
|
||||
const allowedIds = new Set();
|
||||
@@ -247,41 +275,6 @@ export class AccessCore {
|
||||
(await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
case Permission.ASSET_READ:
|
||||
return (
|
||||
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
||||
);
|
||||
case Permission.ASSET_UPDATE:
|
||||
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
||||
|
||||
case Permission.ASSET_DELETE:
|
||||
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
||||
|
||||
case Permission.ASSET_RESTORE:
|
||||
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
||||
|
||||
case Permission.ASSET_SHARE:
|
||||
return (
|
||||
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
case Permission.ASSET_VIEW:
|
||||
return (
|
||||
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
case Permission.ASSET_DOWNLOAD:
|
||||
return (
|
||||
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
|
||||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user