[Doc] More documentation on decorator and plugins roles and auth*

This commit is contained in:
Ferdinand Thiessen 2020-10-30 04:05:59 +01:00
parent a5d3b837cd
commit 8a9776ae0e
6 changed files with 134 additions and 65 deletions

View File

@ -10,8 +10,8 @@ def get_all():
return Role.query.all() return Role.query.all()
def get(rid): def get(role_name):
role = Role.query.get(rid).one_or_none() role = Role.query.filter(Role.name == role_name).one_or_none()
if not role: if not role:
raise NotFound raise NotFound
@ -42,11 +42,11 @@ def create_permissions(permissions):
db.session.commit() db.session.commit()
def create_role(name, permissions=[]): def create_role(name: str, permissions=[]):
role = Role(name=name) role = Role(name=name)
db.session.add(role) db.session.add(role)
set_permissions(role, permissions) set_permissions(role, permissions)
return role.id return role
def delete(role): def delete(role):

View File

@ -7,6 +7,15 @@ from flaschengeist.controller import sessionController
def login_required(permission=None): 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): def wrap(func):
@wraps(func) @wraps(func)
def wrapped_f(*args, **kwargs): def wrapped_f(*args, **kwargs):

View File

@ -28,8 +28,8 @@ def login():
POST-data: {'userid': string, 'password': string} POST-data: {'userid': string, 'password': string}
Returns: Returns:
A JSON object with `flaschengeist.system.models.user.User` and created A JSON object with `flaschengeist.models.user.User` and created
`flaschengeist.system.models.session.Session` or HTTP error `flaschengeist.models.session.Session` or HTTP error
""" """
logger.debug("Start log in.") logger.debug("Start log in.")
data = request.get_json() data = request.get_json()
@ -60,7 +60,7 @@ def get_sessions(current_session, **kwargs):
Route: ``/auth`` | Method: ``GET`` Route: ``/auth`` | Method: ``GET``
Returns: 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) sessions = sessionController.get_users_sessions(current_session._user)
return jsonify(sessions) return jsonify(sessions)
@ -100,7 +100,7 @@ def get_session(token, current_session, **kwargs):
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
Returns: 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)) logger.debug("get token {{ {} }}".format(token))
session = sessionController.get_session(token, current_session._user) 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 current_session: Session sent with Authorization Header
Returns: 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)) logger.debug("get token {{ {} }}".format(token))
session = sessionController.get_session(token, current_session._user) session = sessionController.get_session(token, current_session._user)

View File

@ -1,3 +1,5 @@
"""LDAP Authentication Provider Plugin"""
import ssl import ssl
from ldap3.utils.hashed import hashed from ldap3.utils.hashed import hashed
from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5 from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5

View File

@ -1,36 +1,40 @@
import binascii """Authentication Provider Plugin
import hashlib Allows simple authentication using Username-Password pair with password saved into
import os Flaschengeist database (as User attribute)
"""
import os
import hashlib
import binascii
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from flaschengeist.plugins import AuthPlugin from flaschengeist.plugins import AuthPlugin
from flaschengeist.models.user import User 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): class AuthPlain(AuthPlugin):
def login(self, user: User, password: str): def login(self, user: User, password: str):
if user.has_attribute("password"): if user.has_attribute("password"):
return _verify_password(user.get_attribute("password"), password) return AuthPlain._verify_password(user.get_attribute("password"), password)
return False return False
def modify_user(self, user, password, new_password=None): def modify_user(self, user, password, new_password=None):
if password is not None and not self.login(user, password): if password is not None and not self.login(user, password):
raise BadRequest raise BadRequest
if new_password: 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

View File

@ -1,3 +1,8 @@
"""Roles plugin
Provides routes used to configure roles and permissions of users / roles.
"""
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from werkzeug.exceptions import NotFound, BadRequest from werkzeug.exceptions import NotFound, BadRequest
@ -15,57 +20,98 @@ class RolesPlugin(Plugin):
super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete]) super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete])
###################################################### @roles_bp.route("/roles", methods=["GET"])
# Routes # @login_required()
# # def list_roles(current_session):
# /roles POST: register new # """List all existing roles
# GET: get all roles #
# /roles/permissions GET: get all permissions # Route: ``/roles`` | Method: ``GET``
# /roles/<rid> GET: get role with rid #
# PUT: modify role / permission # Args:
# DELETE: remove role # 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"]) @roles_bp.route("/roles", methods=["POST"])
@login_required(permission=_permission_edit) @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() data = request.get_json()
if not data or "name" not in data: if not data or "name" not in data:
raise BadRequest raise BadRequest
if "permissions" in data: if "permissions" in data:
permissions = data["permissions"] permissions = data["permissions"]
role = roleController.create_role(data["name"], permissions) 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)
@roles_bp.route("/roles/permissions", methods=["GET"]) @roles_bp.route("/roles/permissions", methods=["GET"])
@login_required() @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() permissions = roleController.get_permissions()
return jsonify(permissions) return jsonify(permissions)
@roles_bp.route("/roles/<rid>", methods=["GET"]) @roles_bp.route("/roles/<role_name>", methods=["GET"])
@login_required() @login_required()
def __get_role(rid, **kwargs): def get_role(role_name, current_session):
role = roleController.get(rid) """Get role by name
if role:
return jsonify({"id": role.id, "name": role, "permissions": role.permissions}) Route: ``/roles/<role_name>`` | Method: ``GET``
raise NotFound
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/<rid>", methods=["PUT"]) @roles_bp.route("/roles/<role_name>", methods=["PUT"])
@login_required(permission=_permission_edit) @login_required(permission=_permission_edit)
def __edit_role(rid, **kwargs): def edit_role(role_name, current_session):
role = roleController.get(rid) """Edit role, rename and / or set permissions
Route: ``/roles/<role_name>`` | 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() data = request.get_json()
if "name" in data: if "name" in data:
@ -73,13 +119,21 @@ def __edit_role(rid, **kwargs):
if "permissions" in data: if "permissions" in data:
roleController.set_permissions(role, data["permissions"]) roleController.set_permissions(role, data["permissions"])
roleController.update_role(role) roleController.update_role(role)
return jsonify({"ok": "ok"})
@roles_bp.route("/roles/<rid>", methods=["DELETE"]) @roles_bp.route("/roles/<role_name>", methods=["DELETE"])
@login_required(permission=_permission_delete) @login_required(permission=_permission_delete)
def __delete_role(rid, **kwargs): def delete_role(role_name, current_session):
role = roleController.get(rid) """Delete role
roleController.delete(role)
return jsonify({"ok": "ok"}) Route: ``/roles/<role_name>`` | 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)