Permission System (#47)

* feat(api): permissions system

Adds a permission system for isAuthenticated middleware. Also adds user CRUD.
pull/48/head
sct 4 years ago committed by GitHub
parent 5d46f8d76d
commit cfc84ce2f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -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,13 +17,18 @@ 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[]
res.status(403).json({ ): Middleware => {
status: 403, const authMiddleware: Middleware = (req, res, next) => {
error: 'You do not have permisson to access this endpoint', if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
}); res.status(403).json({
} else { status: 403,
next(); error: 'You do not have permisson to access this endpoint',
} });
} else {
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;

Loading…
Cancel
Save