From 0fdeac041753831ea0c9402ed8df8f58aef0fb28 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 25 Jul 2025 15:25:23 -0400 Subject: [PATCH] feat!: more permissions (#20250) feat: more api key permissions --- e2e/src/api/specs/album.e2e-spec.ts | 4 +- mobile/openapi/lib/model/permission.dart | 162 ++++++++++++++++-- open-api/immich-openapi-specs.json | 54 +++++- open-api/typescript-sdk/src/fetch-client.ts | 54 +++++- server/src/controllers/album.controller.ts | 10 +- .../src/controllers/asset-media.controller.ts | 12 +- server/src/controllers/asset.controller.ts | 14 +- server/src/controllers/auth.controller.ts | 10 +- server/src/controllers/download.controller.ts | 5 +- .../src/controllers/duplicate.controller.ts | 7 +- server/src/controllers/job.controller.ts | 7 +- server/src/controllers/memory.controller.ts | 6 +- server/src/controllers/search.controller.ts | 19 +- server/src/controllers/server.controller.ts | 27 +-- server/src/controllers/sync.controller.ts | 9 +- server/src/controllers/user.controller.ts | 32 ++-- server/src/enum.ts | 70 +++++++- .../1753464178233-RenameApiKeyPermissions.ts | 22 +++ server/src/services/album.service.ts | 4 +- server/src/utils/access.ts | 6 +- 20 files changed, 414 insertions(+), 120 deletions(-) create mode 100644 server/src/schema/migrations/1753464178233-RenameApiKeyPermissions.ts diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index eedf70dc58..af9b17cc13 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -470,7 +470,7 @@ describe('/albums', () => { .send({ ids: [asset.id] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access')); + expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.create access')); }); it('should add duplicate assets only once', async () => { @@ -599,7 +599,7 @@ describe('/albums', () => { .send({ ids: [user1Asset1.id] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access')); + expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.delete access')); }); it('should remove duplicate assets only once', async () => { diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index a85b5002bf..ec67d81be4 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -36,25 +36,35 @@ class Permission { static const assetPeriodRead = Permission._(r'asset.read'); static const assetPeriodUpdate = Permission._(r'asset.update'); static const assetPeriodDelete = Permission._(r'asset.delete'); + static const assetPeriodStatistics = Permission._(r'asset.statistics'); static const assetPeriodShare = Permission._(r'asset.share'); static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodUpload = Permission._(r'asset.upload'); + static const assetPeriodReplace = Permission._(r'asset.replace'); static const albumPeriodCreate = Permission._(r'album.create'); static const albumPeriodRead = Permission._(r'album.read'); static const albumPeriodUpdate = Permission._(r'album.update'); static const albumPeriodDelete = Permission._(r'album.delete'); static const albumPeriodStatistics = Permission._(r'album.statistics'); - static const albumPeriodAddAsset = Permission._(r'album.addAsset'); - static const albumPeriodRemoveAsset = Permission._(r'album.removeAsset'); static const albumPeriodShare = Permission._(r'album.share'); static const albumPeriodDownload = Permission._(r'album.download'); + static const albumAssetPeriodCreate = Permission._(r'albumAsset.create'); + static const albumAssetPeriodDelete = Permission._(r'albumAsset.delete'); + static const albumUserPeriodCreate = Permission._(r'albumUser.create'); + static const albumUserPeriodUpdate = Permission._(r'albumUser.update'); + static const albumUserPeriodDelete = Permission._(r'albumUser.delete'); + static const authPeriodChangePassword = Permission._(r'auth.changePassword'); static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); static const archivePeriodRead = Permission._(r'archive.read'); + static const duplicatePeriodRead = Permission._(r'duplicate.read'); + static const duplicatePeriodDelete = Permission._(r'duplicate.delete'); static const facePeriodCreate = Permission._(r'face.create'); static const facePeriodRead = Permission._(r'face.read'); static const facePeriodUpdate = Permission._(r'face.update'); static const facePeriodDelete = Permission._(r'face.delete'); + static const jobPeriodCreate = Permission._(r'job.create'); + static const jobPeriodRead = Permission._(r'job.read'); static const libraryPeriodCreate = Permission._(r'library.create'); static const libraryPeriodRead = Permission._(r'library.read'); static const libraryPeriodUpdate = Permission._(r'library.update'); @@ -66,6 +76,9 @@ class Permission { static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodUpdate = Permission._(r'memory.update'); static const memoryPeriodDelete = Permission._(r'memory.delete'); + static const memoryPeriodStatistics = Permission._(r'memory.statistics'); + static const memoryAssetPeriodCreate = Permission._(r'memoryAsset.create'); + static const memoryAssetPeriodDelete = Permission._(r'memoryAsset.delete'); static const notificationPeriodCreate = Permission._(r'notification.create'); static const notificationPeriodRead = Permission._(r'notification.read'); static const notificationPeriodUpdate = Permission._(r'notification.update'); @@ -81,6 +94,16 @@ class Permission { static const personPeriodStatistics = Permission._(r'person.statistics'); static const personPeriodMerge = Permission._(r'person.merge'); static const personPeriodReassign = Permission._(r'person.reassign'); + static const pinCodePeriodCreate = Permission._(r'pinCode.create'); + static const pinCodePeriodUpdate = Permission._(r'pinCode.update'); + static const pinCodePeriodDelete = Permission._(r'pinCode.delete'); + static const serverPeriodAbout = Permission._(r'server.about'); + static const serverPeriodApkLinks = Permission._(r'server.apkLinks'); + static const serverPeriodStorage = Permission._(r'server.storage'); + static const serverPeriodStatistics = Permission._(r'server.statistics'); + static const serverLicensePeriodRead = Permission._(r'serverLicense.read'); + static const serverLicensePeriodUpdate = Permission._(r'serverLicense.update'); + static const serverLicensePeriodDelete = Permission._(r'serverLicense.delete'); static const sessionPeriodCreate = Permission._(r'session.create'); static const sessionPeriodRead = Permission._(r'session.read'); static const sessionPeriodUpdate = Permission._(r'session.update'); @@ -94,6 +117,10 @@ class Permission { static const stackPeriodRead = Permission._(r'stack.read'); static const stackPeriodUpdate = Permission._(r'stack.update'); static const stackPeriodDelete = Permission._(r'stack.delete'); + static const syncPeriodStream = Permission._(r'sync.stream'); + static const syncCheckpointPeriodRead = Permission._(r'syncCheckpoint.read'); + static const syncCheckpointPeriodUpdate = Permission._(r'syncCheckpoint.update'); + static const syncCheckpointPeriodDelete = Permission._(r'syncCheckpoint.delete'); static const systemConfigPeriodRead = Permission._(r'systemConfig.read'); static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update'); static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read'); @@ -103,10 +130,25 @@ class Permission { static const tagPeriodUpdate = Permission._(r'tag.update'); static const tagPeriodDelete = Permission._(r'tag.delete'); static const tagPeriodAsset = Permission._(r'tag.asset'); - static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create'); - static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read'); - static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update'); - static const adminPeriodUserPeriodDelete = Permission._(r'admin.user.delete'); + static const userPeriodRead = Permission._(r'user.read'); + static const userPeriodUpdate = Permission._(r'user.update'); + static const userLicensePeriodCreate = Permission._(r'userLicense.create'); + static const userLicensePeriodRead = Permission._(r'userLicense.read'); + static const userLicensePeriodUpdate = Permission._(r'userLicense.update'); + static const userLicensePeriodDelete = Permission._(r'userLicense.delete'); + static const userOnboardingPeriodRead = Permission._(r'userOnboarding.read'); + static const userOnboardingPeriodUpdate = Permission._(r'userOnboarding.update'); + static const userOnboardingPeriodDelete = Permission._(r'userOnboarding.delete'); + static const userPreferencePeriodRead = Permission._(r'userPreference.read'); + static const userPreferencePeriodUpdate = Permission._(r'userPreference.update'); + static const userProfileImagePeriodCreate = Permission._(r'userProfileImage.create'); + static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read'); + static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update'); + static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete'); + static const adminUserPeriodCreate = Permission._(r'adminUser.create'); + static const adminUserPeriodRead = Permission._(r'adminUser.read'); + static const adminUserPeriodUpdate = Permission._(r'adminUser.update'); + static const adminUserPeriodDelete = Permission._(r'adminUser.delete'); /// List of all possible values in this [enum][Permission]. static const values = [ @@ -123,25 +165,35 @@ class Permission { assetPeriodRead, assetPeriodUpdate, assetPeriodDelete, + assetPeriodStatistics, assetPeriodShare, assetPeriodView, assetPeriodDownload, assetPeriodUpload, + assetPeriodReplace, albumPeriodCreate, albumPeriodRead, albumPeriodUpdate, albumPeriodDelete, albumPeriodStatistics, - albumPeriodAddAsset, - albumPeriodRemoveAsset, albumPeriodShare, albumPeriodDownload, + albumAssetPeriodCreate, + albumAssetPeriodDelete, + albumUserPeriodCreate, + albumUserPeriodUpdate, + albumUserPeriodDelete, + authPeriodChangePassword, authDevicePeriodDelete, archivePeriodRead, + duplicatePeriodRead, + duplicatePeriodDelete, facePeriodCreate, facePeriodRead, facePeriodUpdate, facePeriodDelete, + jobPeriodCreate, + jobPeriodRead, libraryPeriodCreate, libraryPeriodRead, libraryPeriodUpdate, @@ -153,6 +205,9 @@ class Permission { memoryPeriodRead, memoryPeriodUpdate, memoryPeriodDelete, + memoryPeriodStatistics, + memoryAssetPeriodCreate, + memoryAssetPeriodDelete, notificationPeriodCreate, notificationPeriodRead, notificationPeriodUpdate, @@ -168,6 +223,16 @@ class Permission { personPeriodStatistics, personPeriodMerge, personPeriodReassign, + pinCodePeriodCreate, + pinCodePeriodUpdate, + pinCodePeriodDelete, + serverPeriodAbout, + serverPeriodApkLinks, + serverPeriodStorage, + serverPeriodStatistics, + serverLicensePeriodRead, + serverLicensePeriodUpdate, + serverLicensePeriodDelete, sessionPeriodCreate, sessionPeriodRead, sessionPeriodUpdate, @@ -181,6 +246,10 @@ class Permission { stackPeriodRead, stackPeriodUpdate, stackPeriodDelete, + syncPeriodStream, + syncCheckpointPeriodRead, + syncCheckpointPeriodUpdate, + syncCheckpointPeriodDelete, systemConfigPeriodRead, systemConfigPeriodUpdate, systemMetadataPeriodRead, @@ -190,10 +259,25 @@ class Permission { tagPeriodUpdate, tagPeriodDelete, tagPeriodAsset, - adminPeriodUserPeriodCreate, - adminPeriodUserPeriodRead, - adminPeriodUserPeriodUpdate, - adminPeriodUserPeriodDelete, + userPeriodRead, + userPeriodUpdate, + userLicensePeriodCreate, + userLicensePeriodRead, + userLicensePeriodUpdate, + userLicensePeriodDelete, + userOnboardingPeriodRead, + userOnboardingPeriodUpdate, + userOnboardingPeriodDelete, + userPreferencePeriodRead, + userPreferencePeriodUpdate, + userProfileImagePeriodCreate, + userProfileImagePeriodRead, + userProfileImagePeriodUpdate, + userProfileImagePeriodDelete, + adminUserPeriodCreate, + adminUserPeriodRead, + adminUserPeriodUpdate, + adminUserPeriodDelete, ]; static Permission? fromJson(dynamic value) => PermissionTypeTransformer().decode(value); @@ -245,25 +329,35 @@ class PermissionTypeTransformer { case r'asset.read': return Permission.assetPeriodRead; case r'asset.update': return Permission.assetPeriodUpdate; case r'asset.delete': return Permission.assetPeriodDelete; + case r'asset.statistics': return Permission.assetPeriodStatistics; case r'asset.share': return Permission.assetPeriodShare; case r'asset.view': return Permission.assetPeriodView; case r'asset.download': return Permission.assetPeriodDownload; case r'asset.upload': return Permission.assetPeriodUpload; + case r'asset.replace': return Permission.assetPeriodReplace; case r'album.create': return Permission.albumPeriodCreate; case r'album.read': return Permission.albumPeriodRead; case r'album.update': return Permission.albumPeriodUpdate; case r'album.delete': return Permission.albumPeriodDelete; case r'album.statistics': return Permission.albumPeriodStatistics; - case r'album.addAsset': return Permission.albumPeriodAddAsset; - case r'album.removeAsset': return Permission.albumPeriodRemoveAsset; case r'album.share': return Permission.albumPeriodShare; case r'album.download': return Permission.albumPeriodDownload; + case r'albumAsset.create': return Permission.albumAssetPeriodCreate; + case r'albumAsset.delete': return Permission.albumAssetPeriodDelete; + case r'albumUser.create': return Permission.albumUserPeriodCreate; + case r'albumUser.update': return Permission.albumUserPeriodUpdate; + case r'albumUser.delete': return Permission.albumUserPeriodDelete; + case r'auth.changePassword': return Permission.authPeriodChangePassword; case r'authDevice.delete': return Permission.authDevicePeriodDelete; case r'archive.read': return Permission.archivePeriodRead; + case r'duplicate.read': return Permission.duplicatePeriodRead; + case r'duplicate.delete': return Permission.duplicatePeriodDelete; case r'face.create': return Permission.facePeriodCreate; case r'face.read': return Permission.facePeriodRead; case r'face.update': return Permission.facePeriodUpdate; case r'face.delete': return Permission.facePeriodDelete; + case r'job.create': return Permission.jobPeriodCreate; + case r'job.read': return Permission.jobPeriodRead; case r'library.create': return Permission.libraryPeriodCreate; case r'library.read': return Permission.libraryPeriodRead; case r'library.update': return Permission.libraryPeriodUpdate; @@ -275,6 +369,9 @@ class PermissionTypeTransformer { case r'memory.read': return Permission.memoryPeriodRead; case r'memory.update': return Permission.memoryPeriodUpdate; case r'memory.delete': return Permission.memoryPeriodDelete; + case r'memory.statistics': return Permission.memoryPeriodStatistics; + case r'memoryAsset.create': return Permission.memoryAssetPeriodCreate; + case r'memoryAsset.delete': return Permission.memoryAssetPeriodDelete; case r'notification.create': return Permission.notificationPeriodCreate; case r'notification.read': return Permission.notificationPeriodRead; case r'notification.update': return Permission.notificationPeriodUpdate; @@ -290,6 +387,16 @@ class PermissionTypeTransformer { case r'person.statistics': return Permission.personPeriodStatistics; case r'person.merge': return Permission.personPeriodMerge; case r'person.reassign': return Permission.personPeriodReassign; + case r'pinCode.create': return Permission.pinCodePeriodCreate; + case r'pinCode.update': return Permission.pinCodePeriodUpdate; + case r'pinCode.delete': return Permission.pinCodePeriodDelete; + case r'server.about': return Permission.serverPeriodAbout; + case r'server.apkLinks': return Permission.serverPeriodApkLinks; + case r'server.storage': return Permission.serverPeriodStorage; + case r'server.statistics': return Permission.serverPeriodStatistics; + case r'serverLicense.read': return Permission.serverLicensePeriodRead; + case r'serverLicense.update': return Permission.serverLicensePeriodUpdate; + case r'serverLicense.delete': return Permission.serverLicensePeriodDelete; case r'session.create': return Permission.sessionPeriodCreate; case r'session.read': return Permission.sessionPeriodRead; case r'session.update': return Permission.sessionPeriodUpdate; @@ -303,6 +410,10 @@ class PermissionTypeTransformer { case r'stack.read': return Permission.stackPeriodRead; case r'stack.update': return Permission.stackPeriodUpdate; case r'stack.delete': return Permission.stackPeriodDelete; + case r'sync.stream': return Permission.syncPeriodStream; + case r'syncCheckpoint.read': return Permission.syncCheckpointPeriodRead; + case r'syncCheckpoint.update': return Permission.syncCheckpointPeriodUpdate; + case r'syncCheckpoint.delete': return Permission.syncCheckpointPeriodDelete; case r'systemConfig.read': return Permission.systemConfigPeriodRead; case r'systemConfig.update': return Permission.systemConfigPeriodUpdate; case r'systemMetadata.read': return Permission.systemMetadataPeriodRead; @@ -312,10 +423,25 @@ class PermissionTypeTransformer { case r'tag.update': return Permission.tagPeriodUpdate; case r'tag.delete': return Permission.tagPeriodDelete; case r'tag.asset': return Permission.tagPeriodAsset; - case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate; - case r'admin.user.read': return Permission.adminPeriodUserPeriodRead; - case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate; - case r'admin.user.delete': return Permission.adminPeriodUserPeriodDelete; + case r'user.read': return Permission.userPeriodRead; + case r'user.update': return Permission.userPeriodUpdate; + case r'userLicense.create': return Permission.userLicensePeriodCreate; + case r'userLicense.read': return Permission.userLicensePeriodRead; + case r'userLicense.update': return Permission.userLicensePeriodUpdate; + case r'userLicense.delete': return Permission.userLicensePeriodDelete; + case r'userOnboarding.read': return Permission.userOnboardingPeriodRead; + case r'userOnboarding.update': return Permission.userOnboardingPeriodUpdate; + case r'userOnboarding.delete': return Permission.userOnboardingPeriodDelete; + case r'userPreference.read': return Permission.userPreferencePeriodRead; + case r'userPreference.update': return Permission.userPreferencePeriodUpdate; + case r'userProfileImage.create': return Permission.userProfileImagePeriodCreate; + case r'userProfileImage.read': return Permission.userProfileImagePeriodRead; + case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate; + case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete; + case r'adminUser.create': return Permission.adminUserPeriodCreate; + case r'adminUser.read': return Permission.adminUserPeriodRead; + case r'adminUser.update': return Permission.adminUserPeriodUpdate; + case r'adminUser.delete': return Permission.adminUserPeriodDelete; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 35f84ec25a..576592413d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11760,25 +11760,35 @@ "asset.read", "asset.update", "asset.delete", + "asset.statistics", "asset.share", "asset.view", "asset.download", "asset.upload", + "asset.replace", "album.create", "album.read", "album.update", "album.delete", "album.statistics", - "album.addAsset", - "album.removeAsset", "album.share", "album.download", + "albumAsset.create", + "albumAsset.delete", + "albumUser.create", + "albumUser.update", + "albumUser.delete", + "auth.changePassword", "authDevice.delete", "archive.read", + "duplicate.read", + "duplicate.delete", "face.create", "face.read", "face.update", "face.delete", + "job.create", + "job.read", "library.create", "library.read", "library.update", @@ -11790,6 +11800,9 @@ "memory.read", "memory.update", "memory.delete", + "memory.statistics", + "memoryAsset.create", + "memoryAsset.delete", "notification.create", "notification.read", "notification.update", @@ -11805,6 +11818,16 @@ "person.statistics", "person.merge", "person.reassign", + "pinCode.create", + "pinCode.update", + "pinCode.delete", + "server.about", + "server.apkLinks", + "server.storage", + "server.statistics", + "serverLicense.read", + "serverLicense.update", + "serverLicense.delete", "session.create", "session.read", "session.update", @@ -11818,6 +11841,10 @@ "stack.read", "stack.update", "stack.delete", + "sync.stream", + "syncCheckpoint.read", + "syncCheckpoint.update", + "syncCheckpoint.delete", "systemConfig.read", "systemConfig.update", "systemMetadata.read", @@ -11827,10 +11854,25 @@ "tag.update", "tag.delete", "tag.asset", - "admin.user.create", - "admin.user.read", - "admin.user.update", - "admin.user.delete" + "user.read", + "user.update", + "userLicense.create", + "userLicense.read", + "userLicense.update", + "userLicense.delete", + "userOnboarding.read", + "userOnboarding.update", + "userOnboarding.delete", + "userPreference.read", + "userPreference.update", + "userProfileImage.create", + "userProfileImage.read", + "userProfileImage.update", + "userProfileImage.delete", + "adminUser.create", + "adminUser.read", + "adminUser.update", + "adminUser.delete" ], "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2918e00dd7..7c030d43c2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -3947,25 +3947,35 @@ export enum Permission { AssetRead = "asset.read", AssetUpdate = "asset.update", AssetDelete = "asset.delete", + AssetStatistics = "asset.statistics", AssetShare = "asset.share", AssetView = "asset.view", AssetDownload = "asset.download", AssetUpload = "asset.upload", + AssetReplace = "asset.replace", AlbumCreate = "album.create", AlbumRead = "album.read", AlbumUpdate = "album.update", AlbumDelete = "album.delete", AlbumStatistics = "album.statistics", - AlbumAddAsset = "album.addAsset", - AlbumRemoveAsset = "album.removeAsset", AlbumShare = "album.share", AlbumDownload = "album.download", + AlbumAssetCreate = "albumAsset.create", + AlbumAssetDelete = "albumAsset.delete", + AlbumUserCreate = "albumUser.create", + AlbumUserUpdate = "albumUser.update", + AlbumUserDelete = "albumUser.delete", + AuthChangePassword = "auth.changePassword", AuthDeviceDelete = "authDevice.delete", ArchiveRead = "archive.read", + DuplicateRead = "duplicate.read", + DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", FaceRead = "face.read", FaceUpdate = "face.update", FaceDelete = "face.delete", + JobCreate = "job.create", + JobRead = "job.read", LibraryCreate = "library.create", LibraryRead = "library.read", LibraryUpdate = "library.update", @@ -3977,6 +3987,9 @@ export enum Permission { MemoryRead = "memory.read", MemoryUpdate = "memory.update", MemoryDelete = "memory.delete", + MemoryStatistics = "memory.statistics", + MemoryAssetCreate = "memoryAsset.create", + MemoryAssetDelete = "memoryAsset.delete", NotificationCreate = "notification.create", NotificationRead = "notification.read", NotificationUpdate = "notification.update", @@ -3992,6 +4005,16 @@ export enum Permission { PersonStatistics = "person.statistics", PersonMerge = "person.merge", PersonReassign = "person.reassign", + PinCodeCreate = "pinCode.create", + PinCodeUpdate = "pinCode.update", + PinCodeDelete = "pinCode.delete", + ServerAbout = "server.about", + ServerApkLinks = "server.apkLinks", + ServerStorage = "server.storage", + ServerStatistics = "server.statistics", + ServerLicenseRead = "serverLicense.read", + ServerLicenseUpdate = "serverLicense.update", + ServerLicenseDelete = "serverLicense.delete", SessionCreate = "session.create", SessionRead = "session.read", SessionUpdate = "session.update", @@ -4005,6 +4028,10 @@ export enum Permission { StackRead = "stack.read", StackUpdate = "stack.update", StackDelete = "stack.delete", + SyncStream = "sync.stream", + SyncCheckpointRead = "syncCheckpoint.read", + SyncCheckpointUpdate = "syncCheckpoint.update", + SyncCheckpointDelete = "syncCheckpoint.delete", SystemConfigRead = "systemConfig.read", SystemConfigUpdate = "systemConfig.update", SystemMetadataRead = "systemMetadata.read", @@ -4014,10 +4041,25 @@ export enum Permission { TagUpdate = "tag.update", TagDelete = "tag.delete", TagAsset = "tag.asset", - AdminUserCreate = "admin.user.create", - AdminUserRead = "admin.user.read", - AdminUserUpdate = "admin.user.update", - AdminUserDelete = "admin.user.delete" + UserRead = "user.read", + UserUpdate = "user.update", + UserLicenseCreate = "userLicense.create", + UserLicenseRead = "userLicense.read", + UserLicenseUpdate = "userLicense.update", + UserLicenseDelete = "userLicense.delete", + UserOnboardingRead = "userOnboarding.read", + UserOnboardingUpdate = "userOnboarding.update", + UserOnboardingDelete = "userOnboarding.delete", + UserPreferenceRead = "userPreference.read", + UserPreferenceUpdate = "userPreference.update", + UserProfileImageCreate = "userProfileImage.create", + UserProfileImageRead = "userProfileImage.read", + UserProfileImageUpdate = "userProfileImage.update", + UserProfileImageDelete = "userProfileImage.delete", + AdminUserCreate = "adminUser.create", + AdminUserRead = "adminUser.read", + AdminUserUpdate = "adminUser.update", + AdminUserDelete = "adminUser.delete" } export enum AssetMediaStatus { Created = "created", diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 2e6d1e7e43..36c5e0b13b 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -67,7 +67,7 @@ export class AlbumController { } @Put(':id/assets') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) addAssetsToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -77,7 +77,7 @@ export class AlbumController { } @Delete(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.AlbumAssetDelete }) removeAssetFromAlbum( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, @@ -87,7 +87,7 @@ export class AlbumController { } @Put(':id/users') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserCreate }) addUsersToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -97,7 +97,7 @@ export class AlbumController { } @Put(':id/user/:userId') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserUpdate }) updateAlbumUser( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -108,7 +108,7 @@ export class AlbumController { } @Delete(':id/user/:userId') - @Authenticated() + @Authenticated({ permission: Permission.AlbumUserDelete }) removeUserFromAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index ea6c9602c8..8e83b77fb0 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -34,7 +34,7 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ImmichHeader, RouteKey } from 'src/enum'; +import { ImmichHeader, Permission, RouteKey } from 'src/enum'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor'; @@ -61,7 +61,7 @@ export class AssetMediaController { required: false, }) @ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto }) - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetUpload, sharedLink: true }) async uploadAsset( @Auth() auth: AuthDto, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @@ -80,7 +80,7 @@ export class AssetMediaController { @Get(':id/original') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) async downloadAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -101,7 +101,7 @@ export class AssetMediaController { summary: 'replaceAsset', description: 'Replace the asset with new file, without changing its id', }) - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetReplace, sharedLink: true }) async replaceAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -120,7 +120,7 @@ export class AssetMediaController { @Get(':id/thumbnail') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) async viewAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -157,7 +157,7 @@ export class AssetMediaController { @Get(':id/video/playback') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) async playAssetVideo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index bb17daddf3..d23785a5ff 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -13,7 +13,7 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { RouteKey } from 'src/enum'; +import { Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AssetService } from 'src/services/asset.service'; import { UUIDParamDto } from 'src/validation'; @@ -24,7 +24,7 @@ export class AssetController { constructor(private service: AssetService) {} @Get('random') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) @EndpointLifecycle({ deprecatedAt: 'v1.116.0' }) getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { return this.service.getRandom(auth, dto.count ?? 1); @@ -44,7 +44,7 @@ export class AssetController { } @Get('statistics') - @Authenticated() + @Authenticated({ permission: Permission.AssetStatistics }) getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise { return this.service.getStatistics(auth, dto); } @@ -58,26 +58,26 @@ export class AssetController { @Put() @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.AssetUpdate }) updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.AssetDelete }) deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id) as Promise; } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.AssetUpdate }) updateAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 9bc5fd0fbb..30b0d662f2 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -16,7 +16,7 @@ import { ValidateAccessTokenResponseDto, } from 'src/dtos/auth.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto'; -import { AuthType, ImmichCookie } from 'src/enum'; +import { AuthType, ImmichCookie, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @@ -57,7 +57,7 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AuthChangePassword }) changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise { return this.service.changePassword(auth, dto); } @@ -87,19 +87,19 @@ export class AuthController { } @Post('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeCreate }) setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise { return this.service.setupPinCode(auth, dto); } @Put('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeUpdate }) async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise { return this.service.changePinCode(auth, dto); } @Delete('pin-code') - @Authenticated() + @Authenticated({ permission: Permission.PinCodeDelete }) async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise { return this.service.resetPinCode(auth, dto); } diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 880e636dd1..4f5b18e585 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { DownloadService } from 'src/services/download.service'; import { asStreamableFile } from 'src/utils/file'; @@ -13,7 +14,7 @@ export class DownloadController { constructor(private service: DownloadService) {} @Post('info') - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise { return this.service.getDownloadInfo(auth, dto); } @@ -21,7 +22,7 @@ export class DownloadController { @Post('archive') @HttpCode(HttpStatus.OK) @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { return this.service.downloadArchive(auth, dto).then(asStreamableFile); } diff --git a/server/src/controllers/duplicate.controller.ts b/server/src/controllers/duplicate.controller.ts index f6b09e6e7a..da6fe4042d 100644 --- a/server/src/controllers/duplicate.controller.ts +++ b/server/src/controllers/duplicate.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { DuplicateService } from 'src/services/duplicate.service'; import { UUIDParamDto } from 'src/validation'; @@ -13,19 +14,19 @@ export class DuplicateController { constructor(private service: DuplicateService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.DuplicateRead }) getAssetDuplicates(@Auth() auth: AuthDto): Promise { return this.service.getDuplicates(auth); } @Delete() - @Authenticated() + @Authenticated({ permission: Permission.DuplicateDelete }) deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.deleteAll(auth, dto); } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.DuplicateDelete }) deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 7da19e207f..e6b40e6810 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; @@ -10,19 +11,19 @@ export class JobController { constructor(private service: JobService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobRead, admin: true }) getAllJobsStatus(): Promise { return this.service.getAllJobsStatus(); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobCreate, admin: true }) createJob(@Body() dto: JobCreateDto): Promise { return this.service.create(dto); } @Put(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.JobCreate, admin: true }) sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise { return this.service.handleCommand(id, dto); } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index a5bbbd7411..786f2af8a4 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -32,7 +32,7 @@ export class MemoryController { } @Get('statistics') - @Authenticated({ permission: Permission.MemoryRead }) + @Authenticated({ permission: Permission.MemoryStatistics }) memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.statistics(auth, dto); } @@ -61,7 +61,7 @@ export class MemoryController { } @Put(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.MemoryAssetCreate }) addMemoryAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -72,7 +72,7 @@ export class MemoryController { @Delete(':id/assets') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.MemoryAssetDelete }) removeMemoryAssets( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 9bda1fcada..fefa916fe7 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -16,6 +16,7 @@ import { SmartSearchDto, StatisticsSearchDto, } from 'src/dtos/search.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SearchService } from 'src/services/search.service'; @@ -26,58 +27,58 @@ export class SearchController { @Post('metadata') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise { return this.service.searchMetadata(auth, dto); } @Post('statistics') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetStatistics }) searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise { return this.service.searchStatistics(auth, dto); } @Post('random') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise { return this.service.searchRandom(auth, dto); } @Post('smart') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise { return this.service.searchSmart(auth, dto); } @Get('explore') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getExploreData(@Auth() auth: AuthDto): Promise { return this.service.getExploreData(auth); } @Get('person') - @Authenticated() + @Authenticated({ permission: Permission.PersonRead }) searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise { return this.service.searchPerson(auth, dto); } @Get('places') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) searchPlaces(@Query() dto: SearchPlacesDto): Promise { return this.service.searchPlaces(dto); } @Get('cities') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getAssetsByCity(@Auth() auth: AuthDto): Promise { return this.service.getAssetsByCity(auth); } @Get('suggestions') - @Authenticated() + @Authenticated({ permission: Permission.AssetRead }) getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { // TODO fix open api generation to indicate that results can be nullable return this.service.getSearchSuggestions(auth, dto) as Promise; diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 3544fce2a0..9a1004c280 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -15,6 +15,7 @@ import { ServerVersionResponseDto, } from 'src/dtos/server.dto'; import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { ServerService } from 'src/services/server.service'; import { SystemMetadataService } from 'src/services/system-metadata.service'; @@ -30,19 +31,19 @@ export class ServerController { ) {} @Get('about') - @Authenticated() + @Authenticated({ permission: Permission.ServerAbout }) getAboutInfo(): Promise { return this.service.getAboutInfo(); } @Get('apk-links') - @Authenticated() + @Authenticated({ permission: Permission.ServerApkLinks }) getApkLinks(): ServerApkLinksDto { return this.service.getApkLinks(); } @Get('storage') - @Authenticated() + @Authenticated({ permission: Permission.ServerStorage }) getStorage(): Promise { return this.service.getStorage(); } @@ -78,7 +79,7 @@ export class ServerController { } @Get('statistics') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerStatistics, admin: true }) getServerStatistics(): Promise { return this.service.getStatistics(); } @@ -88,25 +89,25 @@ export class ServerController { return this.service.getSupportedMediaTypes(); } + @Get('license') + @Authenticated({ permission: Permission.ServerLicenseRead, admin: true }) + @ApiNotFoundResponse() + getServerLicense(): Promise { + return this.service.getLicense(); + } + @Put('license') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerLicenseUpdate, admin: true }) setServerLicense(@Body() license: LicenseKeyDto): Promise { return this.service.setLicense(license); } @Delete('license') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ServerLicenseDelete, admin: true }) deleteServerLicense(): Promise { return this.service.deleteLicense(); } - @Get('license') - @Authenticated({ admin: true }) - @ApiNotFoundResponse() - getServerLicense(): Promise { - return this.service.getLicense(); - } - @Get('version-check') @Authenticated() getVersionCheck(): Promise { diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index 0945810be7..a7b2b21a54 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -12,6 +12,7 @@ import { SyncAckSetDto, SyncStreamDto, } from 'src/dtos/sync.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { SyncService } from 'src/services/sync.service'; @@ -41,7 +42,7 @@ export class SyncController { @Post('stream') @Header('Content-Type', 'application/jsonlines+json') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({ permission: Permission.SyncStream }) async getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) { try { await this.service.stream(auth, res, dto); @@ -52,21 +53,21 @@ export class SyncController { } @Get('ack') - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointRead }) getSyncAck(@Auth() auth: AuthDto): Promise { return this.service.getAcks(auth); } @Post('ack') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointUpdate }) sendSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckSetDto) { return this.service.setAcks(auth, dto); } @Delete('ack') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.SyncCheckpointDelete }) deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto) { return this.service.deleteAcks(auth, dto); } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 76d2cf4c5a..1b91e1a848 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -21,7 +21,7 @@ import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; -import { RouteKey } from 'src/enum'; +import { Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -38,31 +38,31 @@ export class UserController { ) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) searchUsers(@Auth() auth: AuthDto): Promise { return this.service.search(auth); } @Get('me') - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) getMyUser(@Auth() auth: AuthDto): Promise { return this.service.getMe(auth); } @Put('me') - @Authenticated() + @Authenticated({ permission: Permission.UserUpdate }) updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise { return this.service.updateMe(auth, dto); } @Get('me/preferences') - @Authenticated() + @Authenticated({ permission: Permission.UserPreferenceRead }) getMyPreferences(@Auth() auth: AuthDto): Promise { return this.service.getMyPreferences(auth); } @Put('me/preferences') - @Authenticated() + @Authenticated({ permission: Permission.UserPreferenceUpdate }) updateMyPreferences( @Auth() auth: AuthDto, @Body() dto: UserPreferencesUpdateDto, @@ -71,43 +71,43 @@ export class UserController { } @Get('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseRead }) getUserLicense(@Auth() auth: AuthDto): Promise { return this.service.getLicense(auth); } @Put('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseUpdate }) async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise { return this.service.setLicense(auth, license); } @Delete('me/license') - @Authenticated() + @Authenticated({ permission: Permission.UserLicenseDelete }) async deleteUserLicense(@Auth() auth: AuthDto): Promise { await this.service.deleteLicense(auth); } @Get('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingRead }) getUserOnboarding(@Auth() auth: AuthDto): Promise { return this.service.getOnboarding(auth); } @Put('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingUpdate }) async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise { return this.service.setOnboarding(auth, Onboarding); } @Delete('me/onboarding') - @Authenticated() + @Authenticated({ permission: Permission.UserOnboardingDelete }) async deleteUserOnboarding(@Auth() auth: AuthDto): Promise { await this.service.deleteOnboarding(auth); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.UserRead }) getUser(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @@ -116,7 +116,7 @@ export class UserController { @ApiConsumes('multipart/form-data') @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @Post('profile-image') - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageUpdate }) createProfileImage( @Auth() auth: AuthDto, @UploadedFile() fileInfo: Express.Multer.File, @@ -126,14 +126,14 @@ export class UserController { @Delete('profile-image') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageDelete }) deleteProfileImage(@Auth() auth: AuthDto): Promise { return this.service.deleteProfileImage(auth); } @Get(':id/profile-image') @FileResponse() - @Authenticated() + @Authenticated({ permission: Permission.UserProfileImageRead }) async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) { await sendFile(res, next, () => this.service.getProfileImage(id), this.logger); } diff --git a/server/src/enum.ts b/server/src/enum.ts index 75a96dfe67..70e94ed43f 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -87,31 +87,45 @@ export enum Permission { AssetRead = 'asset.read', AssetUpdate = 'asset.update', AssetDelete = 'asset.delete', + AssetStatistics = 'asset.statistics', AssetShare = 'asset.share', AssetView = 'asset.view', AssetDownload = 'asset.download', AssetUpload = 'asset.upload', + AssetReplace = 'asset.replace', AlbumCreate = 'album.create', AlbumRead = 'album.read', AlbumUpdate = 'album.update', AlbumDelete = 'album.delete', AlbumStatistics = 'album.statistics', - - AlbumAddAsset = 'album.addAsset', - AlbumRemoveAsset = 'album.removeAsset', AlbumShare = 'album.share', AlbumDownload = 'album.download', + AlbumAssetCreate = 'albumAsset.create', + AlbumAssetDelete = 'albumAsset.delete', + + AlbumUserCreate = 'albumUser.create', + AlbumUserUpdate = 'albumUser.update', + AlbumUserDelete = 'albumUser.delete', + + AuthChangePassword = 'auth.changePassword', + AuthDeviceDelete = 'authDevice.delete', ArchiveRead = 'archive.read', + DuplicateRead = 'duplicate.read', + DuplicateDelete = 'duplicate.delete', + FaceCreate = 'face.create', FaceRead = 'face.read', FaceUpdate = 'face.update', FaceDelete = 'face.delete', + JobCreate = 'job.create', + JobRead = 'job.read', + LibraryCreate = 'library.create', LibraryRead = 'library.read', LibraryUpdate = 'library.update', @@ -125,6 +139,10 @@ export enum Permission { MemoryRead = 'memory.read', MemoryUpdate = 'memory.update', MemoryDelete = 'memory.delete', + MemoryStatistics = 'memory.statistics', + + MemoryAssetCreate = 'memoryAsset.create', + MemoryAssetDelete = 'memoryAsset.delete', NotificationCreate = 'notification.create', NotificationRead = 'notification.read', @@ -144,6 +162,19 @@ export enum Permission { PersonMerge = 'person.merge', PersonReassign = 'person.reassign', + PinCodeCreate = 'pinCode.create', + PinCodeUpdate = 'pinCode.update', + PinCodeDelete = 'pinCode.delete', + + ServerAbout = 'server.about', + ServerApkLinks = 'server.apkLinks', + ServerStorage = 'server.storage', + ServerStatistics = 'server.statistics', + + ServerLicenseRead = 'serverLicense.read', + ServerLicenseUpdate = 'serverLicense.update', + ServerLicenseDelete = 'serverLicense.delete', + SessionCreate = 'session.create', SessionRead = 'session.read', SessionUpdate = 'session.update', @@ -160,6 +191,11 @@ export enum Permission { StackUpdate = 'stack.update', StackDelete = 'stack.delete', + SyncStream = 'sync.stream', + SyncCheckpointRead = 'syncCheckpoint.read', + SyncCheckpointUpdate = 'syncCheckpoint.update', + SyncCheckpointDelete = 'syncCheckpoint.delete', + SystemConfigRead = 'systemConfig.read', SystemConfigUpdate = 'systemConfig.update', @@ -172,10 +208,30 @@ export enum Permission { TagDelete = 'tag.delete', TagAsset = 'tag.asset', - AdminUserCreate = 'admin.user.create', - AdminUserRead = 'admin.user.read', - AdminUserUpdate = 'admin.user.update', - AdminUserDelete = 'admin.user.delete', + UserRead = 'user.read', + UserUpdate = 'user.update', + + UserLicenseCreate = 'userLicense.create', + UserLicenseRead = 'userLicense.read', + UserLicenseUpdate = 'userLicense.update', + UserLicenseDelete = 'userLicense.delete', + + UserOnboardingRead = 'userOnboarding.read', + UserOnboardingUpdate = 'userOnboarding.update', + UserOnboardingDelete = 'userOnboarding.delete', + + UserPreferenceRead = 'userPreference.read', + UserPreferenceUpdate = 'userPreference.update', + + UserProfileImageCreate = 'userProfileImage.create', + UserProfileImageRead = 'userProfileImage.read', + UserProfileImageUpdate = 'userProfileImage.update', + UserProfileImageDelete = 'userProfileImage.delete', + + AdminUserCreate = 'adminUser.create', + AdminUserRead = 'adminUser.read', + AdminUserUpdate = 'adminUser.update', + AdminUserDelete = 'adminUser.delete', } export enum SharedLinkType { diff --git a/server/src/schema/migrations/1753464178233-RenameApiKeyPermissions.ts b/server/src/schema/migrations/1753464178233-RenameApiKeyPermissions.ts new file mode 100644 index 0000000000..0293a96607 --- /dev/null +++ b/server/src/schema/migrations/1753464178233-RenameApiKeyPermissions.ts @@ -0,0 +1,22 @@ +import { Kysely, sql } from 'kysely'; + +const items = [ + { oldName: 'album.addAsset', newName: 'albumAsset.create' }, + { oldName: 'album.removeAsset', newName: 'albumAsset.delete' }, + { oldName: 'admin.user.create', newName: 'adminUser.create' }, + { oldName: 'admin.user.read', newName: 'adminUser.read' }, + { oldName: 'admin.user.update', newName: 'adminUser.update' }, + { oldName: 'admin.user.delete', newName: 'adminUser.delete' }, +]; + +export async function up(db: Kysely): Promise { + for (const { oldName, newName } of items) { + await sql`UPDATE "api_key" SET "permissions" = array_replace("permissions", ${oldName}, ${newName})`.execute(db); + } +} + +export async function down(db: Kysely): Promise { + for (const { oldName, newName } of items) { + await sql`UPDATE "api_key" SET "permissions" = array_replace("permissions", ${newName}, ${oldName})`.execute(db); + } +} diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index e4f2a44bfd..90aefa6d72 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -158,7 +158,7 @@ export class AlbumService extends BaseService { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.requireAccess({ auth, permission: Permission.AlbumAddAsset, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] }); const results = await addAssets( auth, @@ -187,7 +187,7 @@ export class AlbumService extends BaseService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.AlbumRemoveAsset, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumAssetDelete, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 08ff81e840..8427da6f1b 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -92,7 +92,7 @@ const checkSharedLinkAccess = async ( return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); } - case Permission.AlbumAddAsset: { + case Permission.AlbumAssetCreate: { return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); } @@ -163,7 +163,7 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return setUnion(isOwner, isShared); } - case Permission.AlbumAddAsset: { + case Permission.AlbumAssetCreate: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id, @@ -195,7 +195,7 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return setUnion(isOwner, isShared); } - case Permission.AlbumRemoveAsset: { + case Permission.AlbumAssetDelete: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id,