diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index e5524d99ba..e4e154cf7a 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -337,7 +337,9 @@ export class MediaRequest { type: MediaType.MOVIE, media, requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request + // If the user has the "auto approve" permission, automatically approve the request + // Note: MANAGE_REQUESTS is intentionally excluded to allow third-party tools + // to intercept admin requests for additional processing status: user.hasPermission( [ requestBody.is4k @@ -346,7 +348,6 @@ export class MediaRequest { requestBody.is4k ? Permission.AUTO_APPROVE_4K_MOVIE : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, ], { type: 'or' } ) @@ -360,7 +361,6 @@ export class MediaRequest { requestBody.is4k ? Permission.AUTO_APPROVE_4K_MOVIE : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, ], { type: 'or' } ) @@ -447,7 +447,9 @@ export class MediaRequest { type: MediaType.TV, media, requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request + // If the user has the "auto approve" permission, automatically approve the request + // Note: MANAGE_REQUESTS is intentionally excluded to allow third-party tools + // to intercept admin requests for additional processing status: user.hasPermission( [ requestBody.is4k @@ -456,7 +458,6 @@ export class MediaRequest { requestBody.is4k ? Permission.AUTO_APPROVE_4K_TV : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, ], { type: 'or' } ) @@ -470,7 +471,6 @@ export class MediaRequest { requestBody.is4k ? Permission.AUTO_APPROVE_4K_TV : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, ], { type: 'or' } ) @@ -494,7 +494,6 @@ export class MediaRequest { requestBody.is4k ? Permission.AUTO_APPROVE_4K_TV : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, ], { type: 'or' } ) diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index edc9f7e183..5fe8fc9e5b 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -35,40 +35,86 @@ export interface PermissionCheckOptions { type: 'and' | 'or'; } +/** + * Checks if a permission is an auto-approve permission. + * Admin users should NOT automatically bypass these permissions, + * allowing third-party tools to intercept and process requests. + */ +function isAutoApprovePermission(perm: Permission): boolean { + return ( + perm === Permission.AUTO_APPROVE || + perm === Permission.AUTO_APPROVE_MOVIE || + perm === Permission.AUTO_APPROVE_TV || + perm === Permission.AUTO_APPROVE_4K || + perm === Permission.AUTO_APPROVE_4K_MOVIE || + perm === Permission.AUTO_APPROVE_4K_TV + ); +} + /** * Takes a Permission and the users permission value and determines * if the user has access to the permission provided. If the user has - * the admin permission, true will always be returned from this check! + * the admin permission, true will be returned UNLESS the permission + * being checked is an auto-approve permission (to allow third-party + * tools to intercept requests). * * @param permissions Single permission or array of permissions - * @param value users current permission value + * @param userPermissionValue users current permission value * @param options Extra options to control permission check behavior (mainly for arrays) */ export const hasPermission = ( permissions: Permission | Permission[], - value: number, + userPermissionValue: number, options: PermissionCheckOptions = { type: 'and' } ): boolean => { - let total = 0; - // If we are not checking any permissions, bail out and return true + // This handles isAuthenticated() called with no arguments (any logged-in user) if (permissions === 0) { return true; } + // Normalize permissions to an array + const requiredPermissions: Permission[] = Array.isArray(permissions) + ? permissions + : [permissions]; + + // If we're checking an empty array, return true + if (requiredPermissions.length === 0) { + return true; + } + + // Handle array of permissions if (Array.isArray(permissions)) { - if (value & Permission.ADMIN) { + // Check if this array includes ANY auto-approve permission + const includesAutoApprove = requiredPermissions.some((perm) => + isAutoApprovePermission(perm) + ); + + // If there's NO auto-approve permission in the list, admin bypasses + if (!includesAutoApprove && (userPermissionValue & Permission.ADMIN)) { return true; } + + // Otherwise, do the normal bit checks for each required permission switch (options.type) { case 'and': - return permissions.every((permission) => !!(value & permission)); + return requiredPermissions.every( + (perm) => !!(userPermissionValue & perm) + ); case 'or': - return permissions.some((permission) => !!(value & permission)); + return requiredPermissions.some( + (perm) => !!(userPermissionValue & perm) + ); } - } else { - total = permissions; } - return !!(value & Permission.ADMIN) || !!(value & total); + // Handle single permission + const singlePerm = requiredPermissions[0]; + // If it's NOT an auto-approve permission, let admin pass automatically + if (!isAutoApprovePermission(singlePerm) && (userPermissionValue & Permission.ADMIN)) { + return true; + } + + // Otherwise, must explicitly match the permission bit + return !!(userPermissionValue & singlePerm); }; diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index be73a0dfd5..7ec976b97f 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -45,7 +45,7 @@ class WatchlistSync { [ Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_MOVIE, - Permission.AUTO_APPROVE_TV, + Permission.AUTO_REQUEST_TV, ], { type: 'or' } ) diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 50ea7c5a5f..235ba39271 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -684,6 +684,16 @@ userSettingsRoutes.get<{ id: string }, { permissions?: number }>( } ); +// Auto-approve permission bits that the owner can modify on themselves +// This allows admins to opt out of auto-approval for third-party tool integration +const AUTO_APPROVE_BITS = + Permission.AUTO_APPROVE | + Permission.AUTO_APPROVE_MOVIE | + Permission.AUTO_APPROVE_TV | + Permission.AUTO_APPROVE_4K | + Permission.AUTO_APPROVE_4K_MOVIE | + Permission.AUTO_APPROVE_4K_TV; + userSettingsRoutes.post< { id: string }, { permissions?: number }, @@ -703,7 +713,20 @@ userSettingsRoutes.post< return next({ status: 404, message: 'User not found.' }); } - // "Owner" user permissions cannot be modified, and users cannot set their own permissions + // Special case: Owner (ID 1) can modify their OWN auto-approve permissions + // This allows admins to opt out of auto-approval if desired + if (user.id === 1 && req.user?.id === 1) { + // Only allow changes to auto-approve bits, preserve all other permissions + const currentNonAutoApprove = user.permissions & ~AUTO_APPROVE_BITS; + const newAutoApprove = req.body.permissions & AUTO_APPROVE_BITS; + user.permissions = currentNonAutoApprove | newAutoApprove; + + await userRepository.save(user); + return res.status(200).json({ permissions: user.permissions }); + } + + // "Owner" user permissions cannot be modified by others, + // and users cannot set their own permissions (except owner for auto-approve above) if (user.id === 1 || req.user?.id === user.id) { return next({ status: 403, diff --git a/src/components/PermissionOption/index.tsx b/src/components/PermissionOption/index.tsx index 922bde87d2..1de8dc24e2 100644 --- a/src/components/PermissionOption/index.tsx +++ b/src/components/PermissionOption/index.tsx @@ -48,15 +48,18 @@ const PermissionOption = ({ let disabled = false; let checked = hasPermission(option.permission, currentPermission); + // Check if this is an auto-approve permission + const isAutoApprove = autoApprovePermissions.includes(option.permission); + if ( - // Permissions for user ID 1 (Plex server owner) cannot be changed - (currentUser && currentUser.id === 1) || + // Permissions for user ID 1 (server owner) cannot be changed + // EXCEPT for auto-approve permissions (admins can opt out of auto-approval) + (currentUser && currentUser.id === 1 && !isAutoApprove) || // Admin permission automatically bypasses/grants all other permissions + // EXCEPT for auto-approve permissions (admins must explicitly have these) (option.permission !== Permission.ADMIN && + !isAutoApprove && hasPermission(Permission.ADMIN, currentPermission)) || - // Manage Requests permission automatically grants all Auto-Approve permissions - (autoApprovePermissions.includes(option.permission) && - hasPermission(Permission.MANAGE_REQUESTS, currentPermission)) || // Selecting a parent permission automatically selects all children (!!parent?.permission && hasPermission(parent.permission, currentPermission)) diff --git a/src/components/RequestModal/CollectionRequestModal.tsx b/src/components/RequestModal/CollectionRequestModal.tsx index 0af8f7ca74..15fb8f9193 100644 --- a/src/components/RequestModal/CollectionRequestModal.tsx +++ b/src/components/RequestModal/CollectionRequestModal.tsx @@ -248,9 +248,10 @@ const CollectionRequestModal = ({ is4k, ]); + // Note: MANAGE_REQUESTS is intentionally excluded to allow third-party tools + // to intercept admin requests for additional processing const hasAutoApprove = hasPermission( [ - Permission.MANAGE_REQUESTS, is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE, is4k ? Permission.AUTO_APPROVE_4K_MOVIE : Permission.AUTO_APPROVE_MOVIE, ], diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index d431065ac6..ce25cb36df 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -305,9 +305,10 @@ const MovieRequestModal = ({ ); } + // Note: MANAGE_REQUESTS is intentionally excluded to allow third-party tools + // to intercept admin requests for additional processing const hasAutoApprove = hasPermission( [ - Permission.MANAGE_REQUESTS, is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE, is4k ? Permission.AUTO_APPROVE_4K_MOVIE : Permission.AUTO_APPROVE_MOVIE, ], diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 0a6495854b..19ca125a8c 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -476,9 +476,10 @@ const TvRequestModal = ({ username: editRequest?.requestedBy.displayName, }) : null} + {/* Note: MANAGE_REQUESTS is intentionally excluded to allow third-party tools + to intercept admin requests for additional processing */} {hasPermission( [ - Permission.MANAGE_REQUESTS, is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE, is4k ? Permission.AUTO_APPROVE_4K_TV : Permission.AUTO_APPROVE_TV, ],