mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
Permission System (#47)
* feat(api): permissions system Adds a permission system for isAuthenticated middleware. Also adds user CRUD.
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -12,7 +12,7 @@
|
||||
"previewLimit": 50,
|
||||
"driver": "SQLite",
|
||||
"name": "Local SQLite",
|
||||
"database": "./db/db.sqlite3"
|
||||
"database": "./config/db/db.sqlite3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Permission, hasPermission } from '../lib/permissions';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@@ -12,6 +13,8 @@ export class User {
|
||||
return users.map((u) => u.filter());
|
||||
}
|
||||
|
||||
static readonly filteredFields: string[] = ['plexToken'];
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@@ -21,6 +24,9 @@ export class User {
|
||||
@Column({ nullable: true })
|
||||
public plexToken?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
public permissions = 0;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@@ -32,11 +38,17 @@ export class User {
|
||||
}
|
||||
|
||||
public filter(): Partial<User> {
|
||||
return {
|
||||
id: this.id,
|
||||
email: this.email,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
const filtered: Partial<User> = Object.assign(
|
||||
{},
|
||||
...(Object.keys(this) as (keyof User)[])
|
||||
.filter((k) => !User.filteredFields.includes(k))
|
||||
.map((k) => ({ [k]: this[k] }))
|
||||
);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public hasPermission(permissions: Permission | Permission[]): boolean {
|
||||
return !!hasPermission(permissions, this.permissions);
|
||||
}
|
||||
}
|
||||
|
38
server/lib/permissions.ts
Normal file
38
server/lib/permissions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export enum Permission {
|
||||
NONE = 0,
|
||||
ADMIN = 2,
|
||||
MANAGE_SETTINGS = 4,
|
||||
MANAGE_USERS = 8,
|
||||
MANAGE_REQUESTS = 16,
|
||||
REQUEST = 32,
|
||||
VOTE = 64,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const hasPermission = (
|
||||
permissions: Permission | Permission[],
|
||||
value: number
|
||||
): boolean => {
|
||||
let total = 0;
|
||||
|
||||
// If we are not checking any permissions, bail out and return true
|
||||
if (permissions === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(permissions)) {
|
||||
// Combine all permission values into one
|
||||
total = permissions.reduce((a, v) => a + v, 0);
|
||||
} else {
|
||||
total = permissions;
|
||||
}
|
||||
|
||||
return !!(value & Permission.ADMIN) || !!(value & total);
|
||||
};
|
@@ -1,5 +1,6 @@
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import { Permission } from '../lib/permissions';
|
||||
|
||||
export const checkUser: Middleware = async (req, _res, next) => {
|
||||
if (req.session?.userId) {
|
||||
@@ -16,13 +17,18 @@ export const checkUser: Middleware = async (req, _res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
export const isAuthenticated: Middleware = async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
res.status(403).json({
|
||||
status: 403,
|
||||
error: 'You do not have permisson to access this endpoint',
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
export const isAuthenticated = (
|
||||
permissions?: Permission | Permission[]
|
||||
): Middleware => {
|
||||
const authMiddleware: Middleware = (req, res, next) => {
|
||||
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
|
||||
res.status(403).json({
|
||||
status: 403,
|
||||
error: 'You do not have permisson to access this endpoint',
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
return authMiddleware;
|
||||
};
|
||||
|
@@ -13,20 +13,28 @@ components:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
readOnly: true
|
||||
email:
|
||||
type: string
|
||||
example: 'hey@itsme.com'
|
||||
plexToken:
|
||||
type: string
|
||||
readOnly: true
|
||||
permissions:
|
||||
type: number
|
||||
example: 0
|
||||
createdAt:
|
||||
type: string
|
||||
example: '2020-09-02T05:02:23.000Z'
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
example: '2020-09-02T05:02:23.000Z'
|
||||
readOnly: true
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- permissions
|
||||
- createdAt
|
||||
- updatedAt
|
||||
MainSettings:
|
||||
@@ -478,5 +486,94 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
post:
|
||||
summary: Create a new user
|
||||
description: |
|
||||
Creates a new user. Should under normal circumstances never be called as you will not have a valid authToken to provide for the user.
|
||||
|
||||
In the future when Plex auth is not required, this will be used to create accounts.
|
||||
|
||||
Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
- users
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
responses:
|
||||
'201':
|
||||
description: The created user in JSON
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
/user/{userId}:
|
||||
get:
|
||||
summary: Retrieve a user by ID
|
||||
description: |
|
||||
Retrieve user details in JSON format. Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Users details in JSON
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
put:
|
||||
summary: Update a user by user ID
|
||||
description: |
|
||||
Update a user with provided values in request body. You cannot update a users plex token through this request.
|
||||
|
||||
Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully updated user details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
delete:
|
||||
summary: Delete a user by user ID
|
||||
description: Deletes a user by provided user ID. Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: User successfully deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
security:
|
||||
- cookieAuth: []
|
||||
|
@@ -3,10 +3,11 @@ import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { Permission } from '../lib/permissions';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
authRoutes.get('/me', isAuthenticated, async (req, res) => {
|
||||
authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
if (!req.user) {
|
||||
return res.status(500).json({
|
||||
@@ -54,7 +55,7 @@ authRoutes.post('/login', async (req, res) => {
|
||||
user = new User({
|
||||
email: account.email,
|
||||
plexToken: account.authToken,
|
||||
// TODO: When we add permissions in #52, set admin here
|
||||
permissions: Permission.ADMIN,
|
||||
});
|
||||
await userRepository.save(user);
|
||||
}
|
||||
|
@@ -3,12 +3,17 @@ import user from './user';
|
||||
import authRoutes from './auth';
|
||||
import { checkUser, isAuthenticated } from '../middleware/auth';
|
||||
import settingsRoutes from './settings';
|
||||
import { Permission } from '../lib/permissions';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(checkUser);
|
||||
router.use('/user', isAuthenticated, user);
|
||||
router.use('/settings', isAuthenticated, settingsRoutes);
|
||||
router.use('/user', isAuthenticated(Permission.MANAGE_USERS), user);
|
||||
router.use(
|
||||
'/settings',
|
||||
isAuthenticated(Permission.MANAGE_SETTINGS),
|
||||
settingsRoutes
|
||||
);
|
||||
router.use('/auth', authRoutes);
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
|
@@ -12,4 +12,65 @@ router.get('/', async (req, res) => {
|
||||
return res.status(200).json(User.filterMany(users));
|
||||
});
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = new User({
|
||||
email: req.body.email,
|
||||
permissions: req.body.permissions,
|
||||
plexToken: '',
|
||||
});
|
||||
await userRepository.save(user);
|
||||
return res.status(201).json(user.filter());
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
return res.status(200).json(user.filter());
|
||||
} catch (e) {
|
||||
next({ status: 404, message: 'User not found' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
Object.assign(user, req.body);
|
||||
await userRepository.save(user);
|
||||
|
||||
return res.status(200).json(user.filter());
|
||||
} catch (e) {
|
||||
next({ status: 404, message: 'User not found' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
await userRepository.delete(user.id);
|
||||
return res.status(200).json(user.filter());
|
||||
} catch (e) {
|
||||
next({ status: 404, message: 'User not found' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
Reference in New Issue
Block a user