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,
"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);
}
}

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

Loading…
Cancel
Save