mirror of
https://github.com/Art051/immich.git
synced 2025-08-11 19:29:00 +00:00
Modify Access repository, to evaluate `authDevice`, `library`, `partner`,
`person`, and `timeline` permissions in bulk.
Queries have been validated to match what they currently generate for
single ids.
As an extra performance improvement, we now use a custom QueryBuilder
for the Partners queries, to avoid the eager relationships that add
unneeded `LEFT JOIN` clauses. We only filter based on the ids present in
the `partners` table, so those joins can be avoided.
Queries:
* `library` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" = $1
AND "LibraryEntity"."ownerId" = $2
AND "LibraryEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
SELECT "LibraryEntity"."id" AS "LibraryEntity_id"
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" IN ($1, $2)
AND "LibraryEntity"."ownerId" = $3
AND "LibraryEntity"."deletedAt" IS NULL
```
* `library` 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__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"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `authDevice` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" = $2
)
LIMIT 1
-- After
SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2, $3)
```
* `timeline` 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__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"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `person` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" = $1
AND "PersonEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT "PersonEntity"."id" AS "PersonEntity_id"
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" IN ($1, $2)
AND "PersonEntity"."ownerId" = $3
```
* `partner` update 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__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"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
326 lines
8.7 KiB
TypeScript
326 lines
8.7 KiB
TypeScript
import { IAccessRepository } from '@app/domain';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { In, Repository } from 'typeorm';
|
|
import {
|
|
ActivityEntity,
|
|
AlbumEntity,
|
|
AssetEntity,
|
|
LibraryEntity,
|
|
PartnerEntity,
|
|
PersonEntity,
|
|
SharedLinkEntity,
|
|
UserTokenEntity,
|
|
} from '../entities';
|
|
|
|
export class AccessRepository implements IAccessRepository {
|
|
constructor(
|
|
@InjectRepository(ActivityEntity) private activityRepository: Repository<ActivityEntity>,
|
|
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
|
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
|
|
@InjectRepository(LibraryEntity) private libraryRepository: Repository<LibraryEntity>,
|
|
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
|
|
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
|
@InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
|
|
@InjectRepository(UserTokenEntity) private tokenRepository: Repository<UserTokenEntity>,
|
|
) {}
|
|
|
|
activity = {
|
|
hasOwnerAccess: (userId: string, activityId: string): Promise<boolean> => {
|
|
return this.activityRepository.exist({
|
|
where: {
|
|
id: activityId,
|
|
userId,
|
|
},
|
|
});
|
|
},
|
|
hasAlbumOwnerAccess: (userId: string, activityId: string): Promise<boolean> => {
|
|
return this.activityRepository.exist({
|
|
where: {
|
|
id: activityId,
|
|
album: {
|
|
ownerId: userId,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
hasCreateAccess: (userId: string, albumId: string): Promise<boolean> => {
|
|
return this.albumRepository.exist({
|
|
where: [
|
|
{
|
|
id: albumId,
|
|
isActivityEnabled: true,
|
|
sharedUsers: {
|
|
id: userId,
|
|
},
|
|
},
|
|
{
|
|
id: albumId,
|
|
isActivityEnabled: true,
|
|
ownerId: userId,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
};
|
|
|
|
library = {
|
|
checkOwnerAccess: async (userId: string, libraryIds: Set<string>): Promise<Set<string>> => {
|
|
if (libraryIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.libraryRepository
|
|
.find({
|
|
select: { id: true },
|
|
where: {
|
|
id: In([...libraryIds]),
|
|
ownerId: userId,
|
|
},
|
|
})
|
|
.then((libraries) => new Set(libraries.map((library) => library.id)));
|
|
},
|
|
|
|
checkPartnerAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
|
|
if (partnerIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.partnerRepository
|
|
.createQueryBuilder('partner')
|
|
.select('partner.sharedById')
|
|
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
|
|
.andWhere('partner.sharedWithId = :userId', { userId })
|
|
.getMany()
|
|
.then((partners) => new Set(partners.map((partner) => partner.sharedById)));
|
|
},
|
|
};
|
|
|
|
timeline = {
|
|
checkPartnerAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
|
|
if (partnerIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.partnerRepository
|
|
.createQueryBuilder('partner')
|
|
.select('partner.sharedById')
|
|
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
|
|
.andWhere('partner.sharedWithId = :userId', { userId })
|
|
.getMany()
|
|
.then((partners) => new Set(partners.map((partner) => partner.sharedById)));
|
|
},
|
|
};
|
|
|
|
asset = {
|
|
hasAlbumAccess: (userId: string, assetId: string): Promise<boolean> => {
|
|
return this.albumRepository.exist({
|
|
where: [
|
|
{
|
|
ownerId: userId,
|
|
assets: {
|
|
id: assetId,
|
|
},
|
|
},
|
|
{
|
|
sharedUsers: {
|
|
id: userId,
|
|
},
|
|
assets: {
|
|
id: assetId,
|
|
},
|
|
},
|
|
// still part of a live photo is in an album
|
|
{
|
|
ownerId: userId,
|
|
assets: {
|
|
livePhotoVideoId: assetId,
|
|
},
|
|
},
|
|
{
|
|
sharedUsers: {
|
|
id: userId,
|
|
},
|
|
assets: {
|
|
livePhotoVideoId: assetId,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
},
|
|
|
|
hasOwnerAccess: (userId: string, assetId: string): Promise<boolean> => {
|
|
return this.assetRepository.exist({
|
|
where: {
|
|
id: assetId,
|
|
ownerId: userId,
|
|
},
|
|
withDeleted: true,
|
|
});
|
|
},
|
|
|
|
hasPartnerAccess: (userId: string, assetId: string): Promise<boolean> => {
|
|
return this.partnerRepository.exist({
|
|
where: {
|
|
sharedWith: {
|
|
id: userId,
|
|
},
|
|
sharedBy: {
|
|
assets: {
|
|
id: assetId,
|
|
},
|
|
},
|
|
},
|
|
relations: {
|
|
sharedWith: true,
|
|
sharedBy: {
|
|
assets: true,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
|
|
hasSharedLinkAccess: async (sharedLinkId: string, assetId: string): Promise<boolean> => {
|
|
return this.sharedLinkRepository.exist({
|
|
where: [
|
|
{
|
|
id: sharedLinkId,
|
|
album: {
|
|
assets: {
|
|
id: assetId,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: sharedLinkId,
|
|
assets: {
|
|
id: assetId,
|
|
},
|
|
},
|
|
// still part of a live photo is in a shared link
|
|
{
|
|
id: sharedLinkId,
|
|
album: {
|
|
assets: {
|
|
livePhotoVideoId: assetId,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: sharedLinkId,
|
|
assets: {
|
|
livePhotoVideoId: assetId,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
},
|
|
};
|
|
|
|
authDevice = {
|
|
checkOwnerAccess: async (userId: string, deviceIds: Set<string>): Promise<Set<string>> => {
|
|
if (deviceIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.tokenRepository
|
|
.find({
|
|
select: { id: true },
|
|
where: {
|
|
userId,
|
|
id: In([...deviceIds]),
|
|
},
|
|
})
|
|
.then((tokens) => new Set(tokens.map((token) => token.id)));
|
|
},
|
|
};
|
|
|
|
album = {
|
|
checkOwnerAccess: async (userId: string, albumIds: Set<string>): Promise<Set<string>> => {
|
|
if (albumIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.albumRepository
|
|
.find({
|
|
select: { id: true },
|
|
where: {
|
|
id: In([...albumIds]),
|
|
ownerId: userId,
|
|
},
|
|
})
|
|
.then((albums) => new Set(albums.map((album) => album.id)));
|
|
},
|
|
|
|
checkSharedAlbumAccess: async (userId: string, albumIds: Set<string>): Promise<Set<string>> => {
|
|
if (albumIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.albumRepository
|
|
.find({
|
|
select: { id: true },
|
|
where: {
|
|
id: In([...albumIds]),
|
|
sharedUsers: {
|
|
id: userId,
|
|
},
|
|
},
|
|
})
|
|
.then((albums) => new Set(albums.map((album) => album.id)));
|
|
},
|
|
|
|
checkSharedLinkAccess: async (sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> => {
|
|
if (albumIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.sharedLinkRepository
|
|
.find({
|
|
select: { albumId: true },
|
|
where: {
|
|
id: sharedLinkId,
|
|
albumId: In([...albumIds]),
|
|
},
|
|
})
|
|
.then(
|
|
(sharedLinks) =>
|
|
new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))),
|
|
);
|
|
},
|
|
};
|
|
|
|
person = {
|
|
checkOwnerAccess: async (userId: string, personIds: Set<string>): Promise<Set<string>> => {
|
|
if (personIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.personRepository
|
|
.find({
|
|
select: { id: true },
|
|
where: {
|
|
id: In([...personIds]),
|
|
ownerId: userId,
|
|
},
|
|
})
|
|
.then((persons) => new Set(persons.map((person) => person.id)));
|
|
},
|
|
};
|
|
|
|
partner = {
|
|
checkUpdateAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
|
|
if (partnerIds.size === 0) {
|
|
return new Set();
|
|
}
|
|
|
|
return this.partnerRepository
|
|
.createQueryBuilder('partner')
|
|
.select('partner.sharedById')
|
|
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
|
|
.andWhere('partner.sharedWithId = :userId', { userId })
|
|
.getMany()
|
|
.then((partners) => new Set(partners.map((partner) => partner.sharedById)));
|
|
},
|
|
};
|
|
}
|