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,
|
"previewLimit": 50,
|
||||||
"driver": "SQLite",
|
"driver": "SQLite",
|
||||||
"name": "Local SQLite",
|
"name": "Local SQLite",
|
||||||
"database": "./db/db.sqlite3"
|
"database": "./config/db/db.sqlite3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { Permission, hasPermission } from '../lib/permissions';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
@@ -12,6 +13,8 @@ export class User {
|
|||||||
return users.map((u) => u.filter());
|
return users.map((u) => u.filter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static readonly filteredFields: string[] = ['plexToken'];
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
public id: number;
|
public id: number;
|
||||||
|
|
||||||
@@ -21,6 +24,9 @@ export class User {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public plexToken?: string;
|
public plexToken?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', default: 0 })
|
||||||
|
public permissions = 0;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
@@ -32,11 +38,17 @@ export class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public filter(): Partial<User> {
|
public filter(): Partial<User> {
|
||||||
return {
|
const filtered: Partial<User> = Object.assign(
|
||||||
id: this.id,
|
{},
|
||||||
email: this.email,
|
...(Object.keys(this) as (keyof User)[])
|
||||||
createdAt: this.createdAt,
|
.filter((k) => !User.filteredFields.includes(k))
|
||||||
updatedAt: this.updatedAt,
|
.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 { getRepository } from 'typeorm';
|
||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
|
import { Permission } from '../lib/permissions';
|
||||||
|
|
||||||
export const checkUser: Middleware = async (req, _res, next) => {
|
export const checkUser: Middleware = async (req, _res, next) => {
|
||||||
if (req.session?.userId) {
|
if (req.session?.userId) {
|
||||||
@@ -16,8 +17,11 @@ export const checkUser: Middleware = async (req, _res, next) => {
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAuthenticated: Middleware = async (req, res, next) => {
|
export const isAuthenticated = (
|
||||||
if (!req.user) {
|
permissions?: Permission | Permission[]
|
||||||
|
): Middleware => {
|
||||||
|
const authMiddleware: Middleware = (req, res, next) => {
|
||||||
|
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
status: 403,
|
status: 403,
|
||||||
error: 'You do not have permisson to access this endpoint',
|
error: 'You do not have permisson to access this endpoint',
|
||||||
@@ -25,4 +29,6 @@ export const isAuthenticated: Middleware = async (req, res, next) => {
|
|||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
return authMiddleware;
|
||||||
};
|
};
|
||||||
|
@@ -13,20 +13,28 @@ components:
|
|||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
example: 1
|
example: 1
|
||||||
|
readOnly: true
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
example: 'hey@itsme.com'
|
example: 'hey@itsme.com'
|
||||||
plexToken:
|
plexToken:
|
||||||
type: string
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
permissions:
|
||||||
|
type: number
|
||||||
|
example: 0
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
example: '2020-09-02T05:02:23.000Z'
|
||||||
|
readOnly: true
|
||||||
updatedAt:
|
updatedAt:
|
||||||
type: string
|
type: string
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
example: '2020-09-02T05:02:23.000Z'
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- email
|
- email
|
||||||
|
- permissions
|
||||||
- createdAt
|
- createdAt
|
||||||
- updatedAt
|
- updatedAt
|
||||||
MainSettings:
|
MainSettings:
|
||||||
@@ -478,5 +486,94 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/User'
|
$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:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
|
@@ -3,10 +3,11 @@ import { getRepository } from 'typeorm';
|
|||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
import PlexTvAPI from '../api/plextv';
|
import PlexTvAPI from '../api/plextv';
|
||||||
import { isAuthenticated } from '../middleware/auth';
|
import { isAuthenticated } from '../middleware/auth';
|
||||||
|
import { Permission } from '../lib/permissions';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
|
|
||||||
authRoutes.get('/me', isAuthenticated, async (req, res) => {
|
authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
@@ -54,7 +55,7 @@ authRoutes.post('/login', async (req, res) => {
|
|||||||
user = new User({
|
user = new User({
|
||||||
email: account.email,
|
email: account.email,
|
||||||
plexToken: account.authToken,
|
plexToken: account.authToken,
|
||||||
// TODO: When we add permissions in #52, set admin here
|
permissions: Permission.ADMIN,
|
||||||
});
|
});
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,17 @@ import user from './user';
|
|||||||
import authRoutes from './auth';
|
import authRoutes from './auth';
|
||||||
import { checkUser, isAuthenticated } from '../middleware/auth';
|
import { checkUser, isAuthenticated } from '../middleware/auth';
|
||||||
import settingsRoutes from './settings';
|
import settingsRoutes from './settings';
|
||||||
|
import { Permission } from '../lib/permissions';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(checkUser);
|
router.use(checkUser);
|
||||||
router.use('/user', isAuthenticated, user);
|
router.use('/user', isAuthenticated(Permission.MANAGE_USERS), user);
|
||||||
router.use('/settings', isAuthenticated, settingsRoutes);
|
router.use(
|
||||||
|
'/settings',
|
||||||
|
isAuthenticated(Permission.MANAGE_SETTINGS),
|
||||||
|
settingsRoutes
|
||||||
|
);
|
||||||
router.use('/auth', authRoutes);
|
router.use('/auth', authRoutes);
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
|
@@ -12,4 +12,65 @@ router.get('/', async (req, res) => {
|
|||||||
return res.status(200).json(User.filterMany(users));
|
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;
|
export default router;
|
||||||
|
Reference in New Issue
Block a user