Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions server/entity/MediaRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,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
Expand All @@ -345,7 +347,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -359,7 +360,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand Down Expand Up @@ -446,7 +446,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
Expand All @@ -455,7 +457,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -469,7 +470,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -493,7 +493,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand Down
68 changes: 57 additions & 11 deletions server/lib/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
2 changes: 1 addition & 1 deletion server/lib/watchlistsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class WatchlistSync {
[
Permission.AUTO_REQUEST,
Permission.AUTO_REQUEST_MOVIE,
Permission.AUTO_APPROVE_TV,
Permission.AUTO_REQUEST_TV,
],
{ type: 'or' }
)
Expand Down
25 changes: 24 additions & 1 deletion server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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,
Expand Down
13 changes: 8 additions & 5 deletions src/components/PermissionOption/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/CollectionRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/MovieRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/TvRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,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,
],
Expand Down