From 854a1f61564602b60c95776a70fc517e2f5bfa37 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 20 Oct 2020 18:52:02 +0200 Subject: [PATCH] Improved some permission related stuff, rewrote session controller --- flaschengeist/modules/__init__.py | 3 +- flaschengeist/modules/auth/__init__.py | 22 +-- flaschengeist/modules/roles/__init__.py | 11 +- flaschengeist/modules/schedule/__init__.py | 14 +- flaschengeist/modules/users/__init__.py | 6 +- flaschengeist/system/controller/__init__.py | 7 - .../system/controller/roleController.py | 1 - .../system/controller/sessionController.py | 180 +++++++++--------- flaschengeist/system/decorator.py | 24 +-- 9 files changed, 125 insertions(+), 143 deletions(-) diff --git a/flaschengeist/modules/__init__.py b/flaschengeist/modules/__init__.py index 806bd95..9b085fb 100644 --- a/flaschengeist/modules/__init__.py +++ b/flaschengeist/modules/__init__.py @@ -6,7 +6,7 @@ send_message_hook = HookCall("send_message") class Plugin: - def __init__(self, config=None, blueprint=None, permissions={}): + def __init__(self, config=None, blueprint=None, permissions=[]): self.blueprint = blueprint self.permissions = permissions self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version @@ -20,6 +20,7 @@ class Plugin: def serialize(self): return { "version": self.version, + "permissions": self.permissions } diff --git a/flaschengeist/modules/auth/__init__.py b/flaschengeist/modules/auth/__init__.py index 8f7a426..594bb01 100644 --- a/flaschengeist/modules/auth/__init__.py +++ b/flaschengeist/modules/auth/__init__.py @@ -6,7 +6,6 @@ from flask import Blueprint, request, jsonify from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized -from werkzeug.local import LocalProxy from flaschengeist import logger from flaschengeist.modules import Plugin @@ -14,7 +13,6 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import sessionController, userController, messageController from flaschengeist.system.models.session import Session -session_controller: sessionController.SessionController = LocalProxy(lambda: sessionController.SessionController()) auth_bp = Blueprint("auth", __name__) @@ -56,19 +54,19 @@ def _login(): if not user: raise Unauthorized logger.debug("user is {{ {} }}".format(user)) - session = session_controller.create(user, user_agent=request.user_agent) + session = sessionController.create(user, user_agent=request.user_agent) logger.debug("token is {{ {} }}".format(session.token)) logger.info("User {{ {} }} success login.".format(userid)) # Lets cleanup the DB - session_controller.clear_expired() + sessionController.clear_expired() return jsonify({"session": session, "user": user}) @auth_bp.route("/auth", methods=["GET"]) @login_required() def _get_sessions(access_token: Session, **kwargs): - tokens = session_controller.get_users_sessions(access_token._user) + tokens = sessionController.get_users_sessions(access_token._user) a = messageController.Message(access_token._user, "Go", "Bar") messageController.send_message(a) return jsonify(tokens) @@ -78,14 +76,14 @@ def _get_sessions(access_token: Session, **kwargs): @login_required() def _delete_session(access_token, token, **kwargs): logger.debug("Try to delete access token {{ {} }}".format(token)) - token = session_controller.get_session(token, access_token.user) + token = sessionController.get_session(token, access_token.user) if not token: logger.debug("Token not found in database!") # Return 403 error, so that users can not bruteforce tokens # Valid tokens from other users and invalid tokens now are looking the same raise Forbidden - session_controller.delete_session(token) - session_controller.clear_expired() + sessionController.delete_session(token) + sessionController.clear_expired() return jsonify({"ok": "ok"}) @@ -93,7 +91,7 @@ def _delete_session(access_token, token, **kwargs): @login_required() def _get_session(token, access_token, **kwargs): logger.debug("get token {{ {} }}".format(token)) - session = session_controller.get_session(token, access_token.user) + session = sessionController.get_session(token, access_token.user) if not token: # Return 403 error, so that users can not bruteforce tokens # Valid tokens from other users and invalid tokens now are looking the same @@ -105,7 +103,7 @@ def _get_session(token, access_token, **kwargs): @login_required() def _get_assocd_user(token, access_token, **kwargs): logger.debug("get token {{ {} }}".format(token)) - session = session_controller.get_session(token, access_token.user) + session = sessionController.get_session(token, access_token.user) if not token: # Return 403 error, so that users can not bruteforce tokens # Valid tokens from other users and invalid tokens now are looking the same @@ -116,7 +114,7 @@ def _get_assocd_user(token, access_token, **kwargs): @auth_bp.route("/auth/", methods=["PUT"]) @login_required() def _set_lifetime(token, access_token, **kwargs): - token = session_controller.get_token(token, access_token.user) + token = sessionController.get_token(token, access_token.user) if not token: # Return 403 error, so that users can not bruteforce tokens # Valid tokens from other users and invalid tokens now are looking the same @@ -124,7 +122,7 @@ def _set_lifetime(token, access_token, **kwargs): try: lifetime = request.get_json()["value"] logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token)) - session_controller.set_lifetime(token, lifetime) + sessionController.set_lifetime(token, lifetime) return jsonify({"ok": "ok"}) except (KeyError, TypeError): raise BadRequest diff --git a/flaschengeist/modules/roles/__init__.py b/flaschengeist/modules/roles/__init__.py index 059dfc0..ba332e7 100644 --- a/flaschengeist/modules/roles/__init__.py +++ b/flaschengeist/modules/roles/__init__.py @@ -6,11 +6,12 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import roleController roles_bp = Blueprint("roles", __name__) +roles_permission = "roles_edit" class RolesPlugin(Plugin): def __init__(self, config): - super().__init__(config, roles_bp) + super().__init__(config, roles_bp, permissions=[roles_permission]) ###################################################### @@ -26,8 +27,8 @@ class RolesPlugin(Plugin): @roles_bp.route("/roles", methods=["POST"]) -@login_required() -def add_role(self): +@login_required(permissions=[roles_permission]) +def add_role(**kwargs): data = request.get_json() if not data or "name" not in data: raise BadRequest @@ -61,7 +62,7 @@ def __get_role(rid, **kwargs): @roles_bp.route("/roles/", methods=["PUT"]) -@login_required() +@login_required(permissions=[roles_permission]) def __edit_role(rid, **kwargs): role = roleController.get_role(rid) if not role: @@ -77,7 +78,7 @@ def __edit_role(rid, **kwargs): @roles_bp.route("/roles/", methods=["DELETE"]) -@login_required() +@login_required(permissions=[roles_permission]) def __delete_role(rid, **kwargs): if not roleController.delete_role(rid): raise NotFound diff --git a/flaschengeist/modules/schedule/__init__.py b/flaschengeist/modules/schedule/__init__.py index 9892b7f..70b5a2b 100644 --- a/flaschengeist/modules/schedule/__init__.py +++ b/flaschengeist/modules/schedule/__init__.py @@ -10,11 +10,13 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import eventController schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule") +schedule_perms = {"EDIT_EVENT": "schedule_edit_event", + "NEW_EVENT": "schedule_create_event"} class SchedulePlugin(Plugin): def __init__(self, config): - super().__init__(blueprint=schedule_bp) + super().__init__(blueprint=schedule_bp, permissions=schedule_perms.values()) #################################################################################### @@ -40,8 +42,8 @@ class SchedulePlugin(Plugin): @schedule_bp.route("/events/", methods=["GET"]) -@login_required() # roles=['schedule_read']) -def __get_event(self, id, **kwargs): +@login_required() +def __get_event(id, **kwargs): event = eventController.get_event(id) if not event: raise NotFound @@ -51,7 +53,7 @@ def __get_event(self, id, **kwargs): @schedule_bp.route("/events", methods=["GET"]) @schedule_bp.route("/events//", methods=["GET"]) @schedule_bp.route("/events///", methods=["GET"]) -@login_required() # roles=['schedule_read']) +@login_required() def __get_events(year=datetime.now().year, month=datetime.now().month, day=None, **kwargs): """Get Event objects for specified date (or month or year), if nothing set then events for current month are returned @@ -104,7 +106,7 @@ def __new_slot_kind(**kwargs): @schedule_bp.route("/events", methods=["POST"]) -@login_required() +@login_required(permissions=[schedule_perms["NEW_EVENT"]]) def __new_event(**kwargs): data = request.get_json() event = eventController.create_event( @@ -117,7 +119,7 @@ def __new_event(**kwargs): @schedule_bp.route("/events/", methods=["DELETE"]) -@login_required() +@login_required(permissions=[schedule_perms["EDIT_EVENT"]]) def __delete_event(event_id, **kwargs): if not eventController.delete_event(event_id): raise NotFound diff --git a/flaschengeist/modules/users/__init__.py b/flaschengeist/modules/users/__init__.py index 28f2247..fd2d11c 100644 --- a/flaschengeist/modules/users/__init__.py +++ b/flaschengeist/modules/users/__init__.py @@ -7,12 +7,12 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import userController users_bp = Blueprint("users", __name__) -permissions = {"EDIT_USER": "edit_user"} +users_perm = "users_edit_other" class UsersPlugin(Plugin): def __init__(self, config): - super().__init__(blueprint=users_bp, permissions=permissions) + super().__init__(blueprint=users_bp, permissions=[users_perm]) ################################################# # Routes # @@ -57,7 +57,7 @@ def __edit_user(uid, **kwargs): if not user: raise NotFound - if uid != kwargs["access_token"].user.userid and user.has_permissions(permissions["EDIT_USER"]): + if uid != kwargs["access_token"].user.userid and user.has_permissions([users_perm]): return Forbidden data = request.get_json() diff --git a/flaschengeist/system/controller/__init__.py b/flaschengeist/system/controller/__init__.py index 3776cb9..e69de29 100644 --- a/flaschengeist/system/controller/__init__.py +++ b/flaschengeist/system/controller/__init__.py @@ -1,7 +0,0 @@ -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] diff --git a/flaschengeist/system/controller/roleController.py b/flaschengeist/system/controller/roleController.py index cdea0bc..71baa80 100644 --- a/flaschengeist/system/controller/roleController.py +++ b/flaschengeist/system/controller/roleController.py @@ -1,4 +1,3 @@ -from flask import current_app from sqlalchemy.exc import IntegrityError from werkzeug.exceptions import BadRequest diff --git a/flaschengeist/system/controller/sessionController.py b/flaschengeist/system/controller/sessionController.py index b7607c9..4be467b 100644 --- a/flaschengeist/system/controller/sessionController.py +++ b/flaschengeist/system/controller/sessionController.py @@ -4,116 +4,112 @@ from flaschengeist.system.database import db from flaschengeist import logger from werkzeug.exceptions import Forbidden from datetime import datetime, timezone -from . import Singleton + +lifetime = 1800 -class SessionController(metaclass=Singleton): - """Control all created Sessions +def validate_token(token, user_agent, permissions): + """Verify session - This Class create, delete, find and manage Sessions. + Verify a Session and Roles so if the User has permission or not. + Retrieves the access token if valid else retrieves False - Attributes: - lifetime: Variable for the Lifetime of a Session in seconds. + Args: + token: Token to verify. + user_agent: User agent of browser to check + permissions: Permissions needed to access restricted routes + Returns: + A Session for this given Token or False. """ + logger.debug("check token {{ {} }} is valid".format(token)) + access_token = Session.query.filter_by(token=token).one_or_none() + if access_token: + logger.debug("token found, check if expired or invalid user agent differs") + if access_token.expires >= datetime.now(timezone.utc) and ( + access_token.browser == user_agent.browser and access_token.platform == user_agent.platform + ): + if not permissions or access_token.user.has_permissions(permissions): + access_token.refresh() + db.session.commit() + return access_token + else: + logger.debug("access token is out of date or invalid client used") + delete_session(access_token) + logger.debug("no valid access token with token: {{ {} }} and permissions: {{ {} }}".format(token, permissions)) + return False - def __init__(self, lifetime=1800): - self.lifetime = lifetime - def validate_token(self, token, user_agent, permissions): - """Verify session +def create(user, user_agent=None) -> Session: + """Create a Session - Verify a Session and Roles so if the User has permission or not. - Retrieves the access token if valid else retrieves False + Args: + user: For which User is to create a Session + user_agent: User agent to identify session - Args: - token: Token to verify. - user_agent: User agent of browser to check - permissions: Permissions needed to access restricted routes - Returns: - A Session for this given Token or False. - """ - logger.debug("check token {{ {} }} is valid".format(token)) - access_token = Session.query.filter_by(token=token).one_or_none() - if access_token: - logger.debug("token found, check if expired or invalid user agent differs") - if access_token.expires >= datetime.now(timezone.utc) and ( - access_token.browser == user_agent.browser and access_token.platform == user_agent.platform - ): - if not permissions or access_token.user.has_permissions(permissions): - access_token.refresh() - db.session.commit() - return access_token - else: - logger.debug("access token is out of date or invalid client used") - self.delete_session(access_token) - logger.debug("no valid access token with token: {{ {} }} and permissions: {{ {} }}".format(token, permissions)) - return False + Returns: + Session: A created Token for User + """ + logger.debug("create access token") + token_str = secrets.token_hex(16) + session = Session( + token=token_str, + _user=user, + lifetime=lifetime, + browser=user_agent.browser, + platform=user_agent.platform, + ) + session.refresh() + db.session.add(session) + db.session.commit() + logger.debug("access token is {{ {} }}".format(session.token)) + return session - def create(self, user, user_agent=None) -> Session: - """Create a Session - Args: - user: For which User is to create a Session - user_agent: User agent to identify session +def get_session(token, owner=None): + """Retrieves Session from token string - Returns: - Session: A created Token for User - """ - logger.debug("create access token") - token_str = secrets.token_hex(16) - session = Session( - token=token_str, - _user=user, - lifetime=self.lifetime, - browser=user_agent.browser, - platform=user_agent.platform, - ) - session.refresh() - db.session.add(session) - db.session.commit() - logger.debug("access token is {{ {} }}".format(session.token)) - return session + Args: + token (str): Token string + owner (User, optional): User owning the token - def get_session(self, token, owner=None): - """Retrieves Session from token string + Raises: + Forbidden: Raised if owner is set but does not match + Returns: + Session: Token object identified by given token string + """ + session = Session.query.filter(Session.token == token).one_or_none() + if session and (owner and owner != session.user): + raise Forbidden + return session - Args: - token (str): Token string - owner (User, optional): User owning the token - Raises: - Forbidden: Raised if owner is set but does not match - Returns: - Session: Token object identified by given token string - """ - session = Session.query.filter(Session.token == token).one_or_none() - if session and (owner and owner != session.user): - raise Forbidden - return session +def get_users_sessions(user): + return Session.query.filter(Session._user == user) - def get_users_sessions(self, user): - return Session.query.filter(Session._user == user) - def delete_session(self, token: Session): - """Deletes given Session +def delete_session(token: Session): + """Deletes given Session - Args: - token (Session): Token to delete - """ - db.session.delete(token) - db.session.commit() + Args: + token (Session): Token to delete + """ + db.session.delete(token) + db.session.commit() - def update_session(self, session): - session.refresh() - db.session.commit() - def set_lifetime(self, session, lifetime): - session.lifetime = lifetime - self.update_session(session) +def update_session(session): + session.refresh() + db.session.commit() - def clear_expired(self): - """Remove expired tokens from database""" - logger.debug("Clear expired Sessions") - deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete() - logger.debug("{} sessions have been removed".format(deleted)) - db.session.commit() + +def set_lifetime(session, lifetime): + session.lifetime = lifetime + update_session(session) + + +def clear_expired(): + """Remove expired tokens from database""" + logger.debug("Clear expired Sessions") + deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete() + logger.debug("{} sessions have been removed".format(deleted)) + db.session.commit() diff --git a/flaschengeist/system/decorator.py b/flaschengeist/system/decorator.py index 4b703f5..39b5694 100644 --- a/flaschengeist/system/decorator.py +++ b/flaschengeist/system/decorator.py @@ -3,21 +3,15 @@ from flask import request from werkzeug.exceptions import Unauthorized from flaschengeist import logger +from flaschengeist.system.controller import sessionController -def login_required(**kwargs): - from .controller.sessionController import SessionController - - ac_controller = SessionController() - permissions = None - if "permissions" in kwargs: - permissions = kwargs["roles"] - - def real_decorator(func): +def login_required(permissions=None): + def wrap(func): @wraps(func) - def wrapper(*args, **kwargs): - token = request.headers.get("Authorization").split(" ")[-1] - access_token = ac_controller.validate_token(token, request.user_agent, permissions) + def wrapped_f(*args, **kwargs): + token = list(filter(None, request.headers.get("Authorization").split(" ")))[-1] + access_token = sessionController.validate_token(token, request.user_agent, permissions) if access_token: kwargs["access_token"] = access_token logger.debug("token {{ {} }} is valid".format(token)) @@ -25,7 +19,5 @@ def login_required(**kwargs): else: logger.info("token {{ {} }} is not valid".format(token)) raise Unauthorized - - return wrapper - - return real_decorator + return wrapped_f + return wrap