diff --git a/backend/__init__.py b/backend/__init__.py index bb500ff..b7453ce 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -4,6 +4,7 @@ Backend base module """ import logging import os +from functools import wraps from io import StringIO from logging.config import dictConfig from logging.handlers import MemoryHandler @@ -14,7 +15,7 @@ import jwt import requests from flask import Flask, jsonify, abort from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth -from flask_jwt_extended import JWTManager, decode_token +from flask_jwt_extended import JWTManager, decode_token, get_jwt_identity from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS @@ -179,6 +180,7 @@ CORS(auth_api_bp) logging.getLogger('flask_cors').level = logging.DEBUG + # Fix jwt_extended by 'duck typing' error handlers # jwt_extended._set_error_handler_callbacks(api_v1) # removed for the moment, might raise new (old) problems diff --git a/backend/api/recorder_api.py b/backend/api/recorder_api.py index 3a8609b..3434d7a 100644 --- a/backend/api/recorder_api.py +++ b/backend/api/recorder_api.py @@ -9,9 +9,10 @@ from pprint import pprint from flask_jwt_extended import jwt_required from flask_restx import fields, Resource, inputs -from backend import db, app, LrcException +from backend import db, app, LrcException, Config from backend.api import api_recorder from backend.api.models import recorder_model, recorder_model_model, recorder_command_model +from backend.auth.utils import requires_permission_level from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand from backend.models.room_model import Room import backend.recorder_adapters as r_a @@ -25,6 +26,7 @@ logger = logging.getLogger("lrc.api.recorder") @api_recorder.param('id', 'The recorder identifier') class RecorderResource(Resource): @jwt_required + @requires_permission_level(Config.Permissions.RECODER_SHOW) @api_recorder.doc('get_recorder') @api_recorder.marshal_with(recorder_model, skip_none=False) def get(self, id): @@ -35,6 +37,7 @@ class RecorderResource(Resource): api_recorder.abort(404) @jwt_required + @requires_permission_level(Config.Permissions.RECORDER_DELETE) @api_recorder.doc('delete_todo') @api_recorder.response(204, 'Todo deleted') def delete(self, id): @@ -65,6 +68,7 @@ class RecorderResource(Resource): required=False, store_missing=False) @jwt_required + @requires_permission_level(Config.Permissions.RECORDER_EDIT) @api_recorder.doc('update_recorder') @api_recorder.expect(recorder_model) def put(self, id): @@ -85,6 +89,7 @@ class RecorderResource(Resource): @api_recorder.route('') class RecorderList(Resource): @jwt_required + @requires_permission_level(Config.Permissions.RECORDERS_LIST) @api_recorder.doc('recorders') @api_recorder.marshal_list_with(recorder_model, skip_none=False) def get(self): @@ -95,6 +100,7 @@ class RecorderList(Resource): return Recorder.get_all() @jwt_required + @requires_permission_level(Config.Permissions.RECODER_NEW) @api_recorder.doc('create_recorder') @api_recorder.expect(recorder_model) @api_recorder.marshal_with(recorder_model, skip_none=False, code=201) @@ -161,6 +167,7 @@ class RecorderModelResource(Resource): @api_recorder.route('/model') class RecorderModelList(Resource): @jwt_required + @requires_permission_level(Config.Permissions.RECODER_MODELS_LIST) @api_recorder.doc('recorders') @api_recorder.marshal_list_with(recorder_model_model) def get(self): @@ -172,6 +179,7 @@ class RecorderModelList(Resource): @api_recorder.param('id', 'The recorder command identifier') class RecorderCommandResource(Resource): @jwt_required + @requires_permission_level(Config.Permissions.RECORDER_COMMAND_SHOW) @api_recorder.doc('get_recorder_command') @api_recorder.marshal_with(recorder_command_model) def get(self, id): @@ -186,6 +194,7 @@ class RecorderCommandResource(Resource): recorder_command_model_parser.add_argument('alternative_name', type=str, required=False) @jwt_required + @requires_permission_level(Config.Permissions.RECORDER_COMMAND_EDIT) @api_recorder.doc('update_recorder_command') @api_recorder.expect(recorder_command_model_parser) @api_recorder.marshal_with(recorder_command_model) @@ -201,6 +210,7 @@ class RecorderCommandResource(Resource): @api_recorder.route('/command') class RecorderCommandList(Resource): @jwt_required + @requires_permission_level(Config.Permissions.RECORDER_COMMANDS_LIST) @api_recorder.doc('recorder_commands') @api_recorder.marshal_list_with(recorder_command_model) def get(self): diff --git a/backend/api/user_api.py b/backend/api/user_api.py index dbd70ca..c489b26 100644 --- a/backend/api/user_api.py +++ b/backend/api/user_api.py @@ -14,7 +14,8 @@ from flask_restx import Resource, fields, inputs, abort from backend import db, app, jwt_auth from backend.api import api_user from backend.api.models import user_model, recorder_model, generic_id_parser -from backend.models import Recorder +from backend.auth.utils import requires_permission_level +from backend.models import Recorder, Config from backend.models.user_model import User, Group @@ -90,18 +91,20 @@ class UserList(Resource): # @jwt_auth.login_required @jwt_required + @requires_permission_level(Config.Permissions.USERS_LIST) @api_user.doc('users') @api_user.marshal_list_with(user_model) def get(self): """ - just a test! - :return: Hello: World + returns all users + :return: all users """ current_user = get_jwt_identity() app.logger.info(current_user) return User.get_all() @jwt_required + @requires_permission_level(Config.Permissions.USER_CREATE) @api_user.doc('create_group') @api_user.expect(user_model) @api_user.marshal_with(user_model, code=201) @@ -117,6 +120,7 @@ class UserList(Resource): @api_user.response(404, 'User not found') class UserResource(Resource): @jwt_auth.login_required + @requires_permission_level(Config.Permissions.USER_SHOW) @api_user.doc('get_user') @api_user.marshal_with(user_model) def get(self, id): @@ -126,4 +130,16 @@ class UserResource(Resource): return user api_user.abort(404) + @jwt_auth.login_required + @requires_permission_level(Config.Permissions.USER_DELETE) + @api_user.doc('delete_user') + def delete(self, id): + """Fetch a user given its identifier""" + user = User.get_by_id(id) + if user is not None: + db.session.delete(user) + db.session.commit() + return "ok" + api_user.abort(404) + # api_user.add_resource(UserResource, '/') diff --git a/backend/auth/utils.py b/backend/auth/utils.py index 8a12ca4..0652dd1 100644 --- a/backend/auth/utils.py +++ b/backend/auth/utils.py @@ -2,6 +2,8 @@ import flask_jwt_extended from flask_jwt_extended import jwt_optional, get_jwt_identity from functools import wraps +from flask_restx import abort + from backend import jwt_auth from backend.models.user_model import User @@ -10,26 +12,16 @@ def requires_permission_level(permission_level): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - if flask_jwt_extended.verify_jwt_in_request(): - current_user_id = get_jwt_identity() - user = User.get_by_identifier(current_user_id) - if user is not None: - if user.has_permission(permission_level): - #for g in user.groups: - # if g.permissions - #TODO - pass - else: - pass - # return FALSE - #if not session.get('email'): - # return redirect(url_for('users.login')) - - #user = User.find_by_email(session['email']) - #elif not user.allowed(access_level): - # return redirect(url_for('users.profile', message="You do not have access to that page. Sorry!")) + # if flask_jwt_extended.verify_jwt_in_request(): + current_user_id = get_jwt_identity() + user = User.get_by_identifier(current_user_id) + if user is not None: + if not user.has_permission(permission_level): + abort(401, f"You are missing the permission: {permission_level}") return f(*args, **kwargs) + return decorated_function + return decorator @@ -38,5 +30,7 @@ def require_jwt(): @wraps(f) def decorated_function(*args, **kwargs): return jwt_auth.login_required(jwt_optional(f(*args, **kwargs))) + return decorated_function + return decorator diff --git a/backend/config.py b/backend/config.py index d2a63a9..50500f7 100644 Binary files a/backend/config.py and b/backend/config.py differ diff --git a/backend/models/user_model.py b/backend/models/user_model.py index 5f02ebe..b7e0afe 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -3,6 +3,7 @@ Example user model and related models """ import json +from enum import Enum import sqlalchemy from sqlalchemy.orm import relation, validates @@ -253,12 +254,24 @@ class User(UserMixin, db.Model): @property def effective_permissions(self): - permissions = Config.ROLE_PERMISSION_MAPPINGS.get(self.role, set()) + role_permissions = Config.ROLE_PERMISSION_MAPPINGS.get(self.role, set()) + permissions = set(Permission.query.filter(Permission.name.in_(role_permissions)).all()) + for g in self.groups: for p in g.permissions: permissions.add(p) return permissions + def has_permission(self, permission): + user_permissions = self.effective_permissions + if isinstance(permission, str): + return any([user_permission.name == permission for user_permission in user_permissions]) + if isinstance(permission, Permission): + return any([user_permission.id == permission.id for user_permission in user_permissions]) + if isinstance(permission, Enum): + return any([user_permission.name == str(permission.value) for user_permission in user_permissions]) + return False + @staticmethod def decode_auth_token(auth_token): """