Improved some permission related stuff, rewrote session controller

This commit is contained in:
Ferdinand Thiessen 2020-10-20 18:52:02 +02:00
parent db96d4b178
commit 854a1f6156
9 changed files with 125 additions and 143 deletions

View File

@ -6,7 +6,7 @@ send_message_hook = HookCall("send_message")
class Plugin: class Plugin:
def __init__(self, config=None, blueprint=None, permissions={}): def __init__(self, config=None, blueprint=None, permissions=[]):
self.blueprint = blueprint self.blueprint = blueprint
self.permissions = permissions self.permissions = permissions
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
@ -20,6 +20,7 @@ class Plugin:
def serialize(self): def serialize(self):
return { return {
"version": self.version, "version": self.version,
"permissions": self.permissions
} }

View File

@ -6,7 +6,6 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
from werkzeug.local import LocalProxy
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.modules import Plugin 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.controller import sessionController, userController, messageController
from flaschengeist.system.models.session import Session from flaschengeist.system.models.session import Session
session_controller: sessionController.SessionController = LocalProxy(lambda: sessionController.SessionController())
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
@ -56,19 +54,19 @@ def _login():
if not user: if not user:
raise Unauthorized raise Unauthorized
logger.debug("user is {{ {} }}".format(user)) 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.debug("token is {{ {} }}".format(session.token))
logger.info("User {{ {} }} success login.".format(userid)) logger.info("User {{ {} }} success login.".format(userid))
# Lets cleanup the DB # Lets cleanup the DB
session_controller.clear_expired() sessionController.clear_expired()
return jsonify({"session": session, "user": user}) return jsonify({"session": session, "user": user})
@auth_bp.route("/auth", methods=["GET"]) @auth_bp.route("/auth", methods=["GET"])
@login_required() @login_required()
def _get_sessions(access_token: Session, **kwargs): 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") a = messageController.Message(access_token._user, "Go", "Bar")
messageController.send_message(a) messageController.send_message(a)
return jsonify(tokens) return jsonify(tokens)
@ -78,14 +76,14 @@ def _get_sessions(access_token: Session, **kwargs):
@login_required() @login_required()
def _delete_session(access_token, token, **kwargs): def _delete_session(access_token, token, **kwargs):
logger.debug("Try to delete access token {{ {} }}".format(token)) 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: if not token:
logger.debug("Token not found in database!") logger.debug("Token not found in database!")
# Return 403 error, so that users can not bruteforce tokens # Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same # Valid tokens from other users and invalid tokens now are looking the same
raise Forbidden raise Forbidden
session_controller.delete_session(token) sessionController.delete_session(token)
session_controller.clear_expired() sessionController.clear_expired()
return jsonify({"ok": "ok"}) return jsonify({"ok": "ok"})
@ -93,7 +91,7 @@ def _delete_session(access_token, token, **kwargs):
@login_required() @login_required()
def _get_session(token, access_token, **kwargs): def _get_session(token, access_token, **kwargs):
logger.debug("get token {{ {} }}".format(token)) 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: if not token:
# Return 403 error, so that users can not bruteforce tokens # Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same # 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() @login_required()
def _get_assocd_user(token, access_token, **kwargs): def _get_assocd_user(token, access_token, **kwargs):
logger.debug("get token {{ {} }}".format(token)) 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: if not token:
# Return 403 error, so that users can not bruteforce tokens # Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same # 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/<token>", methods=["PUT"]) @auth_bp.route("/auth/<token>", methods=["PUT"])
@login_required() @login_required()
def _set_lifetime(token, access_token, **kwargs): 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: if not token:
# Return 403 error, so that users can not bruteforce tokens # Return 403 error, so that users can not bruteforce tokens
# Valid tokens from other users and invalid tokens now are looking the same # 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: try:
lifetime = request.get_json()["value"] lifetime = request.get_json()["value"]
logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token)) 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"}) return jsonify({"ok": "ok"})
except (KeyError, TypeError): except (KeyError, TypeError):
raise BadRequest raise BadRequest

View File

@ -6,11 +6,12 @@ from flaschengeist.system.decorator import login_required
from flaschengeist.system.controller import roleController from flaschengeist.system.controller import roleController
roles_bp = Blueprint("roles", __name__) roles_bp = Blueprint("roles", __name__)
roles_permission = "roles_edit"
class RolesPlugin(Plugin): class RolesPlugin(Plugin):
def __init__(self, config): 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"]) @roles_bp.route("/roles", methods=["POST"])
@login_required() @login_required(permissions=[roles_permission])
def add_role(self): def add_role(**kwargs):
data = request.get_json() data = request.get_json()
if not data or "name" not in data: if not data or "name" not in data:
raise BadRequest raise BadRequest
@ -61,7 +62,7 @@ def __get_role(rid, **kwargs):
@roles_bp.route("/roles/<rid>", methods=["PUT"]) @roles_bp.route("/roles/<rid>", methods=["PUT"])
@login_required() @login_required(permissions=[roles_permission])
def __edit_role(rid, **kwargs): def __edit_role(rid, **kwargs):
role = roleController.get_role(rid) role = roleController.get_role(rid)
if not role: if not role:
@ -77,7 +78,7 @@ def __edit_role(rid, **kwargs):
@roles_bp.route("/roles/<rid>", methods=["DELETE"]) @roles_bp.route("/roles/<rid>", methods=["DELETE"])
@login_required() @login_required(permissions=[roles_permission])
def __delete_role(rid, **kwargs): def __delete_role(rid, **kwargs):
if not roleController.delete_role(rid): if not roleController.delete_role(rid):
raise NotFound raise NotFound

View File

@ -10,11 +10,13 @@ from flaschengeist.system.decorator import login_required
from flaschengeist.system.controller import eventController from flaschengeist.system.controller import eventController
schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule") schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule")
schedule_perms = {"EDIT_EVENT": "schedule_edit_event",
"NEW_EVENT": "schedule_create_event"}
class SchedulePlugin(Plugin): class SchedulePlugin(Plugin):
def __init__(self, config): 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/<int:id>", methods=["GET"]) @schedule_bp.route("/events/<int:id>", methods=["GET"])
@login_required() # roles=['schedule_read']) @login_required()
def __get_event(self, id, **kwargs): def __get_event(id, **kwargs):
event = eventController.get_event(id) event = eventController.get_event(id)
if not event: if not event:
raise NotFound 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/<int:year>/<int:month>", methods=["GET"]) @schedule_bp.route("/events/<int:year>/<int:month>", methods=["GET"])
@schedule_bp.route("/events/<int:year>/<int:month>/<int:day>", methods=["GET"]) @schedule_bp.route("/events/<int:year>/<int:month>/<int:day>", methods=["GET"])
@login_required() # roles=['schedule_read']) @login_required()
def __get_events(year=datetime.now().year, month=datetime.now().month, day=None, **kwargs): def __get_events(year=datetime.now().year, month=datetime.now().month, day=None, **kwargs):
"""Get Event objects for specified date (or month or year), """Get Event objects for specified date (or month or year),
if nothing set then events for current month are returned 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"]) @schedule_bp.route("/events", methods=["POST"])
@login_required() @login_required(permissions=[schedule_perms["NEW_EVENT"]])
def __new_event(**kwargs): def __new_event(**kwargs):
data = request.get_json() data = request.get_json()
event = eventController.create_event( event = eventController.create_event(
@ -117,7 +119,7 @@ def __new_event(**kwargs):
@schedule_bp.route("/events/<int:event_id>", methods=["DELETE"]) @schedule_bp.route("/events/<int:event_id>", methods=["DELETE"])
@login_required() @login_required(permissions=[schedule_perms["EDIT_EVENT"]])
def __delete_event(event_id, **kwargs): def __delete_event(event_id, **kwargs):
if not eventController.delete_event(event_id): if not eventController.delete_event(event_id):
raise NotFound raise NotFound

View File

@ -7,12 +7,12 @@ from flaschengeist.system.decorator import login_required
from flaschengeist.system.controller import userController from flaschengeist.system.controller import userController
users_bp = Blueprint("users", __name__) users_bp = Blueprint("users", __name__)
permissions = {"EDIT_USER": "edit_user"} users_perm = "users_edit_other"
class UsersPlugin(Plugin): class UsersPlugin(Plugin):
def __init__(self, config): def __init__(self, config):
super().__init__(blueprint=users_bp, permissions=permissions) super().__init__(blueprint=users_bp, permissions=[users_perm])
################################################# #################################################
# Routes # # Routes #
@ -57,7 +57,7 @@ def __edit_user(uid, **kwargs):
if not user: if not user:
raise NotFound 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 return Forbidden
data = request.get_json() data = request.get_json()

View File

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

View File

@ -1,4 +1,3 @@
from flask import current_app
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest

View File

@ -4,116 +4,112 @@ from flaschengeist.system.database import db
from flaschengeist import logger from flaschengeist import logger
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from datetime import datetime, timezone from datetime import datetime, timezone
from . import Singleton
lifetime = 1800
class SessionController(metaclass=Singleton): def validate_token(token, user_agent, permissions):
"""Control all created Sessions """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: Args:
lifetime: Variable for the Lifetime of a Session in seconds. 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): def create(user, user_agent=None) -> Session:
"""Verify session """Create a Session
Verify a Session and Roles so if the User has permission or not. Args:
Retrieves the access token if valid else retrieves False user: For which User is to create a Session
user_agent: User agent to identify session
Args: Returns:
token: Token to verify. Session: A created Token for User
user_agent: User agent of browser to check """
permissions: Permissions needed to access restricted routes logger.debug("create access token")
Returns: token_str = secrets.token_hex(16)
A Session for this given Token or False. session = Session(
""" token=token_str,
logger.debug("check token {{ {} }} is valid".format(token)) _user=user,
access_token = Session.query.filter_by(token=token).one_or_none() lifetime=lifetime,
if access_token: browser=user_agent.browser,
logger.debug("token found, check if expired or invalid user agent differs") platform=user_agent.platform,
if access_token.expires >= datetime.now(timezone.utc) and ( )
access_token.browser == user_agent.browser and access_token.platform == user_agent.platform session.refresh()
): db.session.add(session)
if not permissions or access_token.user.has_permissions(permissions): db.session.commit()
access_token.refresh() logger.debug("access token is {{ {} }}".format(session.token))
db.session.commit() return session
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
def create(self, user, user_agent=None) -> Session:
"""Create a Session
Args: def get_session(token, owner=None):
user: For which User is to create a Session """Retrieves Session from token string
user_agent: User agent to identify session
Returns: Args:
Session: A created Token for User token (str): Token string
""" owner (User, optional): User owning the token
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
def get_session(self, token, owner=None): Raises:
"""Retrieves Session from token string 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: def get_users_sessions(user):
Forbidden: Raised if owner is set but does not match return Session.query.filter(Session._user == user)
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(self, user):
return Session.query.filter(Session._user == user)
def delete_session(self, token: Session): def delete_session(token: Session):
"""Deletes given Session """Deletes given Session
Args: Args:
token (Session): Token to delete token (Session): Token to delete
""" """
db.session.delete(token) db.session.delete(token)
db.session.commit() db.session.commit()
def update_session(self, session):
session.refresh()
db.session.commit()
def set_lifetime(self, session, lifetime): def update_session(session):
session.lifetime = lifetime session.refresh()
self.update_session(session) db.session.commit()
def clear_expired(self):
"""Remove expired tokens from database""" def set_lifetime(session, lifetime):
logger.debug("Clear expired Sessions") session.lifetime = lifetime
deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete() update_session(session)
logger.debug("{} sessions have been removed".format(deleted))
db.session.commit()
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()

View File

@ -3,21 +3,15 @@ from flask import request
from werkzeug.exceptions import Unauthorized from werkzeug.exceptions import Unauthorized
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.system.controller import sessionController
def login_required(**kwargs): def login_required(permissions=None):
from .controller.sessionController import SessionController def wrap(func):
ac_controller = SessionController()
permissions = None
if "permissions" in kwargs:
permissions = kwargs["roles"]
def real_decorator(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapped_f(*args, **kwargs):
token = request.headers.get("Authorization").split(" ")[-1] token = list(filter(None, request.headers.get("Authorization").split(" ")))[-1]
access_token = ac_controller.validate_token(token, request.user_agent, permissions) access_token = sessionController.validate_token(token, request.user_agent, permissions)
if access_token: if access_token:
kwargs["access_token"] = access_token kwargs["access_token"] = access_token
logger.debug("token {{ {} }} is valid".format(token)) logger.debug("token {{ {} }} is valid".format(token))
@ -25,7 +19,5 @@ def login_required(**kwargs):
else: else:
logger.info("token {{ {} }} is not valid".format(token)) logger.info("token {{ {} }} is not valid".format(token))
raise Unauthorized raise Unauthorized
return wrapped_f
return wrapper return wrap
return real_decorator