# -*- coding: UTF-8 -* """Decorators for permissions, roles and ScoDoc7 Zope compatibility """ from functools import wraps import inspect import werkzeug import flask from flask import g, current_app, request from flask import abort, url_for, redirect from flask_login import current_user from flask_login import login_required import flask_login import app from app.auth.models import User import app.scodoc.sco_utils as scu from app.scodoc.sco_exceptions import ScoValueError class ZUser(object): "Emulating Zope User" def __init__(self): "create, based on `flask_login.current_user`" self.username = current_user.user_name def __str__(self): return self.username def has_permission(self, perm, dept=None): """check if this user as the permission `perm` in departement given by `g.scodoc_dept`. """ raise NotImplementedError() def scodoc(func): """Décorateur pour toutes les fonctions ScoDoc Affecte le département à g et ouvre la connexion à la base Set `g.scodoc_dept` and `g.scodoc_dept_id` if `scodoc_dept` is present in the argument (for routes like `/<scodoc_dept>/Scolarite/sco_exemple`). Else set scodoc_dept=None, scodoc_dept_id=-1. """ @wraps(func) def scodoc_function(*args, **kwargs): # print("@scodoc") # interdit les POST si pas loggué if ( request.method == "POST" and not current_user.is_authenticated and not request.form.get( "__ac_password" ) # exception pour compat API ScoDoc7 ): current_app.logger.info( "POST by non authenticated user (request.form=%s)", str(request.form)[:2048], ) return redirect( url_for( "auth.login", message="La page a expiré. Identifiez-vous et recommencez l'opération", ) ) if "scodoc_dept" in kwargs: dept_acronym = kwargs["scodoc_dept"] # current_app.logger.info("setting dept to " + dept_acronym) app.set_sco_dept(dept_acronym) del kwargs["scodoc_dept"] elif not hasattr(g, "scodoc_dept"): # current_app.logger.info("setting dept to None") g.scodoc_dept = None g.scodoc_dept_id = -1 # invalide return func(*args, **kwargs) return scodoc_function def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): scodoc_dept = getattr(g, "scodoc_dept", None) if not current_user.has_permission(permission, scodoc_dept): return current_app.login_manager.unauthorized() return f(*args, **kwargs) return decorated_function return decorator def permission_required_compat_scodoc7(permission): # XXX TODO A SUPPRIMER """Décorateur pour les fonctions utilisées comme API dans ScoDoc 7 Comme @permission_required mais autorise de passer directement les informations d'auth en paramètres: __ac_name, __ac_password """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): # cherche les paramètre d'auth: # print("@permission_required_compat_scodoc7") auth_ok = False if request.method == "GET": user_name = request.args.get("__ac_name") user_password = request.args.get("__ac_password") elif request.method == "POST": user_name = request.form.get("__ac_name") user_password = request.form.get("__ac_password") else: abort(405) # method not allowed if user_name and user_password: # Ancienne API: va être supprimée courant mars 2023 current_app.logger.warning( "using DEPRECATED ScoDoc7 authentication method !" ) u = User.query.filter_by(user_name=user_name).first() if u and u.check_password(user_password): auth_ok = True flask_login.login_user(u) # reprend le chemin classique: scodoc_dept = getattr(g, "scodoc_dept", None) if not current_user.has_permission(permission, scodoc_dept): abort(403) if auth_ok: return f(*args, **kwargs) else: return login_required(f)(*args, **kwargs) return decorated_function return decorator def admin_required(f): from app.auth.models import Permission return permission_required(Permission.ScoSuperAdmin)(f) def scodoc7func(func): """Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7. Les paramètres de la query string deviennent des (keywords) paramètres de la fonction. """ @wraps(func) def scodoc7func_decorator(*args, **kwargs): """Decorator allowing legacy Zope published methods to be called via Flask routes without modification. There are two cases: the function can be called 1. via a Flask route ("top level call") 2. or be called directly from Python. """ # print("@scodoc7func") # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. top_level = not hasattr(g, "scodoc7_decorated") if not top_level: # ne "redécore" pas return func(*args, **kwargs) g.scodoc7_decorated = True # --- Emulate Zope's REQUEST # REQUEST = ZRequest() # g.zrequest = REQUEST # args from query string (get) or form (post) req_args = scu.get_request_args() ## --- Add positional arguments pos_arg_values = [] argspec = inspect.getfullargspec(func) # current_app.logger.info("argspec=%s" % str(argspec)) nb_default_args = len(argspec.defaults) if argspec.defaults else 0 if nb_default_args: arg_names = argspec.args[:-nb_default_args] else: arg_names = argspec.args for arg_name in arg_names: # pour chaque arg de la fonction vue # peut produire une KeyError s'il manque un argument attendu: try: v = req_args[arg_name] except KeyError as exc: raise ScoValueError(f"argument {arg_name} manquant") from exc # try to convert all arguments to INTEGERS # necessary for db ids and boolean values try: v = int(v) if v else v except (ValueError, TypeError) as exc: if arg_name in { "etudid", "formation_id", "formsemestre_id", "module_id", "moduleimpl_id", "partition_id", "ue_id", }: raise ScoValueError("page introuvable (id invalide)") from exc pos_arg_values.append(v) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values) # current_app.logger.info("req_args=%s" % req_args) # Add keyword arguments if nb_default_args: for arg_name in argspec.args[-nb_default_args:]: # if arg_name == "REQUEST": # special case # kwargs[arg_name] = REQUEST if arg_name in req_args: # set argument kw optionnel v = req_args[arg_name] # try to convert all arguments to INTEGERS # necessary for db ids and boolean values try: v = int(v) except (ValueError, TypeError): pass kwargs[arg_name] = v # current_app.logger.info( # "scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s" # % (top_level, pos_arg_values, kwargs) # ) value = func(*pos_arg_values, **kwargs) if not top_level: return value else: if isinstance(value, werkzeug.wrappers.response.Response): return value # redirected # Build response, adding collected http headers: headers = [] kw = {"response": value, "status": 200} # if g.zrequest: # headers = g.zrequest.RESPONSE.headers # if not headers: # # no customized header, speedup: # return value # if "content-type" in headers: # kw["mimetype"] = headers["content-type"] r = flask.Response(**kw) for h in headers: r.headers[h] = headers[h] return r return scodoc7func_decorator