From 8a9776ae0e5cb08c5a0e10cbe710205aa9516055 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Fri, 30 Oct 2020 04:05:59 +0100 Subject: [PATCH] [Doc] More documentation on decorator and plugins roles and auth* --- flaschengeist/controller/roleController.py | 8 +- flaschengeist/decorator.py | 9 ++ flaschengeist/plugins/auth/__init__.py | 10 +- flaschengeist/plugins/auth_ldap/__init__.py | 2 + flaschengeist/plugins/auth_plain/__init__.py | 44 ++++--- flaschengeist/plugins/roles/__init__.py | 126 +++++++++++++------ 6 files changed, 134 insertions(+), 65 deletions(-) diff --git a/flaschengeist/controller/roleController.py b/flaschengeist/controller/roleController.py index 43c82e8..7100570 100644 --- a/flaschengeist/controller/roleController.py +++ b/flaschengeist/controller/roleController.py @@ -10,8 +10,8 @@ def get_all(): return Role.query.all() -def get(rid): - role = Role.query.get(rid).one_or_none() +def get(role_name): + role = Role.query.filter(Role.name == role_name).one_or_none() if not role: raise NotFound @@ -42,11 +42,11 @@ def create_permissions(permissions): db.session.commit() -def create_role(name, permissions=[]): +def create_role(name: str, permissions=[]): role = Role(name=name) db.session.add(role) set_permissions(role, permissions) - return role.id + return role def delete(role): diff --git a/flaschengeist/decorator.py b/flaschengeist/decorator.py index 02ff521..e9f61f8 100644 --- a/flaschengeist/decorator.py +++ b/flaschengeist/decorator.py @@ -7,6 +7,15 @@ from flaschengeist.controller import sessionController def login_required(permission=None): + """Decorator use to make a route only accessible by logged in users. + Sets ``current_session`` into kwargs of wrapped function with session identified by Authorization header. + + Attributes: + permission: Optional permission needed for this route + + Returns: + Wrapped function with login (and permission) guard + """ def wrap(func): @wraps(func) def wrapped_f(*args, **kwargs): diff --git a/flaschengeist/plugins/auth/__init__.py b/flaschengeist/plugins/auth/__init__.py index d289d5c..593a160 100644 --- a/flaschengeist/plugins/auth/__init__.py +++ b/flaschengeist/plugins/auth/__init__.py @@ -28,8 +28,8 @@ def login(): POST-data: {'userid': string, 'password': string} Returns: - A JSON object with `flaschengeist.system.models.user.User` and created - `flaschengeist.system.models.session.Session` or HTTP error + A JSON object with `flaschengeist.models.user.User` and created + `flaschengeist.models.session.Session` or HTTP error """ logger.debug("Start log in.") data = request.get_json() @@ -60,7 +60,7 @@ def get_sessions(current_session, **kwargs): Route: ``/auth`` | Method: ``GET`` Returns: - A JSON array of `flaschengeist.system.models.session.Session` or HTTP error + A JSON array of `flaschengeist.models.session.Session` or HTTP error """ sessions = sessionController.get_users_sessions(current_session._user) return jsonify(sessions) @@ -100,7 +100,7 @@ def get_session(token, current_session, **kwargs): current_session: Session sent with Authorization Header Returns: - JSON encoded `flaschengeist.system.models.session.Session` or HTTP error + JSON encoded `flaschengeist.models.session.Session` or HTTP error """ logger.debug("get token {{ {} }}".format(token)) session = sessionController.get_session(token, current_session._user) @@ -153,7 +153,7 @@ def get_assocd_user(token, current_session, **kwargs): current_session: Session sent with Authorization Header Returns: - JSON encoded `flaschengeist.system.models.user.User` or HTTP error + JSON encoded `flaschengeist.models.user.User` or HTTP error """ logger.debug("get token {{ {} }}".format(token)) session = sessionController.get_session(token, current_session._user) diff --git a/flaschengeist/plugins/auth_ldap/__init__.py b/flaschengeist/plugins/auth_ldap/__init__.py index 21e9d2d..54d97ea 100644 --- a/flaschengeist/plugins/auth_ldap/__init__.py +++ b/flaschengeist/plugins/auth_ldap/__init__.py @@ -1,3 +1,5 @@ +"""LDAP Authentication Provider Plugin""" + import ssl from ldap3.utils.hashed import hashed from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5 diff --git a/flaschengeist/plugins/auth_plain/__init__.py b/flaschengeist/plugins/auth_plain/__init__.py index 6820831..cdccb5b 100644 --- a/flaschengeist/plugins/auth_plain/__init__.py +++ b/flaschengeist/plugins/auth_plain/__init__.py @@ -1,36 +1,40 @@ -import binascii -import hashlib -import os +"""Authentication Provider Plugin +Allows simple authentication using Username-Password pair with password saved into +Flaschengeist database (as User attribute) +""" +import os +import hashlib +import binascii from werkzeug.exceptions import BadRequest from flaschengeist.plugins import AuthPlugin from flaschengeist.models.user import User -def _hash_password(password): - salt = hashlib.sha256(os.urandom(60)).hexdigest().encode("ascii") - pass_hash = hashlib.pbkdf2_hmac("sha3-512", password.encode("utf-8"), salt, 100000) - pass_hash = binascii.hexlify(pass_hash) - return (salt + pass_hash).decode("ascii") - - -def _verify_password(stored_password, provided_password): - salt = stored_password[:64] - stored_password = stored_password[64:] - pass_hash = hashlib.pbkdf2_hmac("sha3-512", provided_password.encode("utf-8"), salt.encode("ascii"), 100000) - pass_hash = binascii.hexlify(pass_hash).decode("ascii") - return pass_hash == stored_password - - class AuthPlain(AuthPlugin): def login(self, user: User, password: str): if user.has_attribute("password"): - return _verify_password(user.get_attribute("password"), password) + return AuthPlain._verify_password(user.get_attribute("password"), password) return False def modify_user(self, user, password, new_password=None): if password is not None and not self.login(user, password): raise BadRequest if new_password: - user.set_attribute("password", _hash_password(new_password)) + user.set_attribute("password", AuthPlain._hash_password(new_password)) + + @staticmethod + def _hash_password(password): + salt = hashlib.sha256(os.urandom(60)).hexdigest().encode("ascii") + pass_hash = hashlib.pbkdf2_hmac("sha3-512", password.encode("utf-8"), salt, 100000) + pass_hash = binascii.hexlify(pass_hash) + return (salt + pass_hash).decode("ascii") + + @staticmethod + def _verify_password(stored_password, provided_password): + salt = stored_password[:64] + stored_password = stored_password[64:] + pass_hash = hashlib.pbkdf2_hmac("sha3-512", provided_password.encode("utf-8"), salt.encode("ascii"), 100000) + pass_hash = binascii.hexlify(pass_hash).decode("ascii") + return pass_hash == stored_password diff --git a/flaschengeist/plugins/roles/__init__.py b/flaschengeist/plugins/roles/__init__.py index d181025..26588ab 100644 --- a/flaschengeist/plugins/roles/__init__.py +++ b/flaschengeist/plugins/roles/__init__.py @@ -1,3 +1,8 @@ +"""Roles plugin + +Provides routes used to configure roles and permissions of users / roles. +""" + from flask import Blueprint, request, jsonify from werkzeug.exceptions import NotFound, BadRequest @@ -15,57 +20,98 @@ class RolesPlugin(Plugin): super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete]) -###################################################### -# Routes # -# # -# /roles POST: register new # -# GET: get all roles # -# /roles/permissions GET: get all permissions # -# /roles/ GET: get role with rid # -# PUT: modify role / permission # -# DELETE: remove role # -###################################################### +@roles_bp.route("/roles", methods=["GET"]) +@login_required() +def list_roles(current_session): + """List all existing roles + + Route: ``/roles`` | Method: ``GET`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encodes array of `flaschengeist.models.user.Role` + """ + roles = roleController.get_all() + return jsonify(roles) @roles_bp.route("/roles", methods=["POST"]) @login_required(permission=_permission_edit) -def add_role(**kwargs): +def create_role(current_session): + """Create new role + + Route: ``/roles`` | Method: ``POST`` + + POST-data: ``{name: string, permissions?: string[]}`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + HTTP-200 or HTTP error + """ data = request.get_json() if not data or "name" not in data: raise BadRequest if "permissions" in data: permissions = data["permissions"] - role = roleController.create_role(data["name"], permissions) - return jsonify({"ok": "ok", "id": role.id}) - - -@roles_bp.route("/roles", methods=["GET"]) -@login_required() -def list_roles(**kwargs): - roles = roleController.get_all() - return jsonify(roles) + roleController.create_role(data["name"], permissions) @roles_bp.route("/roles/permissions", methods=["GET"]) @login_required() -def list_permissions(**kwargs): +def list_permissions(current_session): + """List all existing permissions + + Route: ``/roles/permissions`` | Method: ``GET`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded list of `flaschengeist.models.user.Permission` + """ permissions = roleController.get_permissions() return jsonify(permissions) -@roles_bp.route("/roles/", methods=["GET"]) +@roles_bp.route("/roles/", methods=["GET"]) @login_required() -def __get_role(rid, **kwargs): - role = roleController.get(rid) - if role: - return jsonify({"id": role.id, "name": role, "permissions": role.permissions}) - raise NotFound +def get_role(role_name, current_session): + """Get role by name + + Route: ``/roles/`` | Method: ``GET`` + + Args: + role_name: Name of role to retrieve + current_session: Session sent with Authorization Header + + Returns: + JSON encoded `flaschengeist.models.user.Role` or HTTP error + """ + role = roleController.get(role_name) + return jsonify(role) -@roles_bp.route("/roles/", methods=["PUT"]) +@roles_bp.route("/roles/", methods=["PUT"]) @login_required(permission=_permission_edit) -def __edit_role(rid, **kwargs): - role = roleController.get(rid) +def edit_role(role_name, current_session): + """Edit role, rename and / or set permissions + + Route: ``/roles/`` | Method: ``PUT`` + + POST-data: ``{name?: string, permissions?: string[]}`` + + Args: + role_name: Name of role + current_session: Session sent with Authorization Header + + Returns: + HTTP-200 or HTTP error + """ + role = roleController.get(role_name) data = request.get_json() if "name" in data: @@ -73,13 +119,21 @@ def __edit_role(rid, **kwargs): if "permissions" in data: roleController.set_permissions(role, data["permissions"]) roleController.update_role(role) - return jsonify({"ok": "ok"}) -@roles_bp.route("/roles/", methods=["DELETE"]) +@roles_bp.route("/roles/", methods=["DELETE"]) @login_required(permission=_permission_delete) -def __delete_role(rid, **kwargs): - role = roleController.get(rid) - roleController.delete(role) +def delete_role(role_name, current_session): + """Delete role - return jsonify({"ok": "ok"}) + Route: ``/roles/`` | Method: ``DELETE`` + + Args: + role_name: Name of role + current_session: Session sent with Authorization Header + + Returns: + HTTP-200 or HTTP error + """ + role = roleController.get(role_name) + roleController.delete(role)