mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
chore(server): Prepare access interfaces for bulk permission checks (#5223)
* chore(server): Prepare access interfaces for bulk permission checks This change adds the `AccessCore.getAllowedIds` method, to evaluate permissions in bulk, along with some other `getAllowedIds*` private methods. The added methods still calculate permissions by id, and are not optimized to reduce the amount of queries and execution time, which will be implemented in separate pull requests. Services that were evaluating permissions in a loop have been refactored to make use of the bulk approach. * chore(server): Apply review suggestions * chore(server): Make multiple-permission check more readable
This commit is contained in:
parent
6e10d15f2c
commit
030cd8c4c4
4 changed files with 61 additions and 34 deletions
|
@ -68,40 +68,48 @@ export class AccessCore {
|
||||||
return authUser;
|
return authUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
||||||
const hasAccess = await this.hasPermission(authUser, permission, ids);
|
ids = Array.isArray(ids) ? ids : [ids];
|
||||||
if (!hasAccess) {
|
const allowedIds = await this.checkAccess(authUser, permission, ids);
|
||||||
|
if (new Set(ids).size !== allowedIds.size) {
|
||||||
throw new BadRequestException(`Not found or no ${permission} access`);
|
throw new BadRequestException(`Not found or no ${permission} access`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) {
|
/**
|
||||||
for (const { permission, id } of permissions) {
|
* Return ids that user has access to, for the given permission.
|
||||||
const hasAccess = await this.hasPermission(authUser, permission, id);
|
* Check is done for each id, and only allowed ids are returned.
|
||||||
if (hasAccess) {
|
*
|
||||||
return true;
|
* @returns Set<string>
|
||||||
}
|
*/
|
||||||
|
async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set<string> | string[]) {
|
||||||
|
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
||||||
|
if (idSet.size === 0) {
|
||||||
|
return new Set();
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
|
||||||
ids = Array.isArray(ids) ? ids : [ids];
|
|
||||||
|
|
||||||
const isSharedLink = authUser.isPublicUser ?? false;
|
const isSharedLink = authUser.isPublicUser ?? false;
|
||||||
|
return isSharedLink
|
||||||
for (const id of ids) {
|
? await this.checkAccessSharedLink(authUser, permission, idSet)
|
||||||
const hasAccess = isSharedLink
|
: await this.checkAccessOther(authUser, permission, idSet);
|
||||||
? await this.hasSharedLinkAccess(authUser, permission, id)
|
|
||||||
: await this.hasOtherAccess(authUser, permission, id);
|
|
||||||
if (!hasAccess) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
|
||||||
|
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) {
|
private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) {
|
||||||
const sharedLinkId = authUser.sharedLinkId;
|
const sharedLinkId = authUser.sharedLinkId;
|
||||||
if (!sharedLinkId) {
|
if (!sharedLinkId) {
|
||||||
|
@ -136,6 +144,18 @@ export class AccessCore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
|
||||||
|
const allowedIds = new Set();
|
||||||
|
for (const id of ids) {
|
||||||
|
const hasAccess = await this.hasOtherAccess(authUser, permission, id);
|
||||||
|
if (hasAccess) {
|
||||||
|
allowedIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk.
|
||||||
private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) {
|
private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) {
|
||||||
switch (permission) {
|
switch (permission) {
|
||||||
// uses album id
|
// uses album id
|
||||||
|
|
|
@ -153,6 +153,8 @@ export class AlbumService {
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
||||||
|
const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id));
|
||||||
|
const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
for (const assetId of dto.ids) {
|
for (const assetId of dto.ids) {
|
||||||
|
@ -162,7 +164,7 @@ export class AlbumService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
|
const hasAccess = allowedAssetIds.has(assetId);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
|
@ -190,6 +192,9 @@ export class AlbumService {
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
||||||
|
const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
|
||||||
|
const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds);
|
||||||
|
const allowedAssetIds = new Set([...canRemove, ...canShare]);
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
for (const assetId of dto.ids) {
|
for (const assetId of dto.ids) {
|
||||||
|
@ -199,10 +204,7 @@ export class AlbumService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await this.access.hasAny(authUser, [
|
const hasAccess = allowedAssetIds.has(assetId);
|
||||||
{ permission: Permission.ALBUM_REMOVE_ASSET, id: assetId },
|
|
||||||
{ permission: Permission.ASSET_SHARE, id: assetId },
|
|
||||||
]);
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -375,10 +375,11 @@ export class PersonService {
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
|
||||||
for (const mergeId of mergeIds) {
|
const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds);
|
||||||
const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId);
|
|
||||||
|
|
||||||
if (!hasPermission) {
|
for (const mergeId of mergeIds) {
|
||||||
|
const hasAccess = allowedIds.has(mergeId);
|
||||||
|
if (!hasAccess) {
|
||||||
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,15 +119,19 @@ export class SharedLinkService {
|
||||||
throw new BadRequestException('Invalid shared link type');
|
throw new BadRequestException('Invalid shared link type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
|
||||||
|
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
|
||||||
|
const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
|
||||||
|
|
||||||
const results: AssetIdsResponseDto[] = [];
|
const results: AssetIdsResponseDto[] = [];
|
||||||
for (const assetId of dto.assetIds) {
|
for (const assetId of dto.assetIds) {
|
||||||
const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId);
|
const hasAsset = existingAssetIds.has(assetId);
|
||||||
if (hasAsset) {
|
if (hasAsset) {
|
||||||
results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
|
results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
|
const hasAccess = allowedAssetIds.has(assetId);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION });
|
results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION });
|
||||||
continue;
|
continue;
|
||||||
|
|
Loading…
Reference in a new issue