mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: new permission to allow users to see other users requests
closes #840
This commit is contained in:
@@ -8,7 +8,11 @@ import {
|
|||||||
RelationCount,
|
RelationCount,
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Permission, hasPermission } from '../lib/permissions';
|
import {
|
||||||
|
Permission,
|
||||||
|
hasPermission,
|
||||||
|
PermissionCheckOptions,
|
||||||
|
} from '../lib/permissions';
|
||||||
import { MediaRequest } from './MediaRequest';
|
import { MediaRequest } from './MediaRequest';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -85,8 +89,11 @@ export class User {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasPermission(permissions: Permission | Permission[]): boolean {
|
public hasPermission(
|
||||||
return !!hasPermission(permissions, this.permissions);
|
permissions: Permission | Permission[],
|
||||||
|
options?: PermissionCheckOptions
|
||||||
|
): boolean {
|
||||||
|
return !!hasPermission(permissions, this.permissions, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public passwordMatch(password: string): Promise<boolean> {
|
public passwordMatch(password: string): Promise<boolean> {
|
||||||
|
@@ -13,6 +13,11 @@ export enum Permission {
|
|||||||
REQUEST_4K_MOVIE = 2048,
|
REQUEST_4K_MOVIE = 2048,
|
||||||
REQUEST_4K_TV = 4096,
|
REQUEST_4K_TV = 4096,
|
||||||
REQUEST_ADVANCED = 8192,
|
REQUEST_ADVANCED = 8192,
|
||||||
|
REQUEST_VIEW = 16384,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionCheckOptions {
|
||||||
|
type: 'and' | 'or';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,10 +27,12 @@ export enum Permission {
|
|||||||
*
|
*
|
||||||
* @param permissions Single permission or array of permissions
|
* @param permissions Single permission or array of permissions
|
||||||
* @param value users current permission value
|
* @param value users current permission value
|
||||||
|
* @param options Extra options to control permission check behavior (mainly for arrays)
|
||||||
*/
|
*/
|
||||||
export const hasPermission = (
|
export const hasPermission = (
|
||||||
permissions: Permission | Permission[],
|
permissions: Permission | Permission[],
|
||||||
value: number
|
value: number,
|
||||||
|
options: PermissionCheckOptions = { type: 'and' }
|
||||||
): boolean => {
|
): boolean => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
@@ -35,8 +42,15 @@ export const hasPermission = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(permissions)) {
|
if (Array.isArray(permissions)) {
|
||||||
// Combine all permission values into one
|
if (value & Permission.ADMIN) {
|
||||||
total = permissions.reduce((a, v) => a + v, 0);
|
return true;
|
||||||
|
}
|
||||||
|
switch (options.type) {
|
||||||
|
case 'and':
|
||||||
|
return permissions.every((permission) => !!(value & permission));
|
||||||
|
case 'or':
|
||||||
|
return permissions.some((permission) => !!(value & permission));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
total = permissions;
|
total = permissions;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
import { Permission } from '../lib/permissions';
|
import { Permission, PermissionCheckOptions } from '../lib/permissions';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
|
|
||||||
export const checkUser: Middleware = async (req, _res, next) => {
|
export const checkUser: Middleware = async (req, _res, next) => {
|
||||||
@@ -34,10 +34,11 @@ export const checkUser: Middleware = async (req, _res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isAuthenticated = (
|
export const isAuthenticated = (
|
||||||
permissions?: Permission | Permission[]
|
permissions?: Permission | Permission[],
|
||||||
|
options?: PermissionCheckOptions
|
||||||
): Middleware => {
|
): Middleware => {
|
||||||
const authMiddleware: Middleware = (req, res, next) => {
|
const authMiddleware: Middleware = (req, res, next) => {
|
||||||
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
|
if (!req.user || !req.user.hasPermission(permissions ?? 0, options)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
status: 403,
|
status: 403,
|
||||||
error: 'You do not have permission to access this endpoint',
|
error: 'You do not have permission to access this endpoint',
|
||||||
|
@@ -57,7 +57,8 @@ requestRoutes.get('/', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [requests, requestCount] = req.user?.hasPermission(
|
const [requests, requestCount] = req.user?.hasPermission(
|
||||||
Permission.MANAGE_REQUESTS
|
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||||
|
{ type: 'or' }
|
||||||
)
|
)
|
||||||
? await requestRepository.findAndCount({
|
? await requestRepository.findAndCount({
|
||||||
order: sortFilter,
|
order: sortFilter,
|
||||||
@@ -102,10 +103,10 @@ requestRoutes.post(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
req.body.userId &&
|
req.body.userId &&
|
||||||
!(
|
!req.user?.hasPermission([
|
||||||
req.user?.hasPermission(Permission.MANAGE_USERS) &&
|
Permission.MANAGE_USERS,
|
||||||
req.user?.hasPermission(Permission.MANAGE_REQUESTS)
|
Permission.MANAGE_REQUESTS,
|
||||||
)
|
])
|
||||||
) {
|
) {
|
||||||
return next({
|
return next({
|
||||||
status: 403,
|
status: 403,
|
||||||
|
@@ -39,6 +39,8 @@ export const messages = defineMessages({
|
|||||||
advancedrequest: 'Advanced Requests',
|
advancedrequest: 'Advanced Requests',
|
||||||
advancedrequestDescription:
|
advancedrequestDescription:
|
||||||
'Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)',
|
'Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)',
|
||||||
|
viewrequests: 'View Requests',
|
||||||
|
viewrequestsDescription: "Grants permission to view other user's requests.",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PermissionEditProps {
|
interface PermissionEditProps {
|
||||||
@@ -85,6 +87,12 @@ export const PermissionEdit: React.FC<PermissionEditProps> = ({
|
|||||||
description: intl.formatMessage(messages.advancedrequestDescription),
|
description: intl.formatMessage(messages.advancedrequestDescription),
|
||||||
permission: Permission.REQUEST_ADVANCED,
|
permission: Permission.REQUEST_ADVANCED,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'viewrequests',
|
||||||
|
name: intl.formatMessage(messages.viewrequests),
|
||||||
|
description: intl.formatMessage(messages.viewrequestsDescription),
|
||||||
|
permission: Permission.REQUEST_VIEW,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -322,8 +322,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
{hasPermission([Permission.MANAGE_REQUESTS, Permission.MANAGE_USERS]) &&
|
||||||
hasPermission(Permission.MANAGE_USERS) &&
|
|
||||||
selectedUser && (
|
selectedUser && (
|
||||||
<div className="mt-0 sm:mt-2">
|
<div className="mt-0 sm:mt-2">
|
||||||
<Listbox
|
<Listbox
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import useSwr from 'swr';
|
import useSwr from 'swr';
|
||||||
import { hasPermission, Permission } from '../../server/lib/permissions';
|
import {
|
||||||
|
hasPermission,
|
||||||
|
Permission,
|
||||||
|
PermissionCheckOptions,
|
||||||
|
} from '../../server/lib/permissions';
|
||||||
import { UserType } from '../../server/constants/user';
|
import { UserType } from '../../server/constants/user';
|
||||||
|
|
||||||
export { Permission, UserType };
|
export { Permission, UserType };
|
||||||
@@ -20,7 +24,10 @@ interface UserHookResponse {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
revalidate: () => Promise<boolean>;
|
revalidate: () => Promise<boolean>;
|
||||||
hasPermission: (permission: Permission | Permission[]) => boolean;
|
hasPermission: (
|
||||||
|
permission: Permission | Permission[],
|
||||||
|
options?: PermissionCheckOptions
|
||||||
|
) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUser = ({
|
export const useUser = ({
|
||||||
@@ -37,8 +44,11 @@ export const useUser = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkPermission = (permission: Permission | Permission[]): boolean => {
|
const checkPermission = (
|
||||||
return hasPermission(permission, data?.permissions ?? 0);
|
permission: Permission | Permission[],
|
||||||
|
options?: PermissionCheckOptions
|
||||||
|
): boolean => {
|
||||||
|
return hasPermission(permission, data?.permissions ?? 0, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -114,6 +114,8 @@
|
|||||||
"components.PermissionEdit.settingsDescription": "Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
|
"components.PermissionEdit.settingsDescription": "Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
|
||||||
"components.PermissionEdit.users": "Manage Users",
|
"components.PermissionEdit.users": "Manage Users",
|
||||||
"components.PermissionEdit.usersDescription": "Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.",
|
"components.PermissionEdit.usersDescription": "Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.",
|
||||||
|
"components.PermissionEdit.viewrequests": "View Requests",
|
||||||
|
"components.PermissionEdit.viewrequestsDescription": "Grants permission to view other user's requests.",
|
||||||
"components.PermissionEdit.vote": "Vote",
|
"components.PermissionEdit.vote": "Vote",
|
||||||
"components.PermissionEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
|
"components.PermissionEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
|
||||||
"components.PersonDetails.appearsin": "Appears in",
|
"components.PersonDetails.appearsin": "Appears in",
|
||||||
|
Reference in New Issue
Block a user