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
11 changes: 4 additions & 7 deletions server/entity/MediaRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ 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 explicit "auto approve" permission, automatically approve the request
// Note: MANAGE_REQUESTS removed so admins can opt out of auto-approval
status: user.hasPermission(
[
requestBody.is4k
Expand All @@ -201,7 +202,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -215,7 +215,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand Down Expand Up @@ -298,7 +297,8 @@ 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 explicit "auto approve" permission, automatically approve the request
// Note: MANAGE_REQUESTS removed so admins can opt out of auto-approval
status: user.hasPermission(
[
requestBody.is4k
Expand All @@ -307,7 +307,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -321,7 +320,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand All @@ -345,7 +343,6 @@ export class MediaRequest {
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
Expand Down
72 changes: 54 additions & 18 deletions server/lib/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,76 @@ export interface PermissionCheckOptions {
type: 'and' | 'or';
}

/**
* 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!
*
* @param permissions Single permission or array of permissions
* @param value users current permission value
* @param options Extra options to control permission check behavior (mainly for arrays)
*/
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
);
}

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;
}

// 1) Normalize permissions to an array
const requiredPermissions: Permission[] = Array.isArray(permissions)
? permissions
: [permissions];

// 2) If we're checking an empty array, return true
if (requiredPermissions.length === 0) {
return true;
}

// 3) If it’s an array of permissions, handle "and"/"or"
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 (!includesAutoApprove && userPermissionValue & Permission.ADMIN) {
// If there's NO auto-approve permission in the list, then
// "admin = true" as usual
return true;
}

// Otherwise, we do the normal bit checks for each required permission
switch (options.type) {
case 'and':
return permissions.every((permission) => !!(value & permission));
// "and": user must have *all* required permissions
return requiredPermissions.every(
(perm) => !!(userPermissionValue & perm)
);
case 'or':
return permissions.some((permission) => !!(value & permission));
// "or": user must have at least one required permission
return requiredPermissions.some(
(perm) => !!(userPermissionValue & perm)
);
}
} else {
total = permissions;
}

return !!(value & Permission.ADMIN) || !!(value & total);
// 4) If it's a single permission (not an array)
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);
};
24 changes: 23 additions & 1 deletion server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ userSettingsRoutes.get<{ id: string }, { permissions?: number }>(
}
);

// Auto-approve permission bits that the owner can modify on themselves
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 @@ -400,7 +409,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
11 changes: 7 additions & 4 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) ||
// 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