Source code for ResearchNotes.auth

# -*- coding: utf-8 -*-
"""
Authorisation model.

Everything with user will go here regarding login/logout and
authorisation to access something (if not done in get_item function.)

Several function are decorators for called views, e.g. login_required or role_required
"""

import functools
import datetime

# import typing
from typing import Callable

# from werkzeug import Response
from werkzeug.security import check_password_hash
from werkzeug.wrappers.response import Response

from flask import (
    Blueprint,
    request,
    flash,
    g,
    redirect,
    render_template,
    session as login_session,  # avoid ambiguities between db_session and login_session
    url_for,
    current_app,
    abort,
)

import ResearchNotes.database_transactions as dbt
from ResearchNotes.database import db, User
from ResearchNotes.form import LoginForm, SearchForm

# Param = typing.ParamSpec("Param")
# RetType = typing.TypeVar("RetType")
# OriginalFunc = Callable[Param, RetType]
# DecoratedFunc = Callable

bp = Blueprint("auth", __name__, url_prefix="/auth")


[docs]@bp.route("/login", methods=("GET", "POST")) def login() -> str | Response: """ Login of registered user. Returns ------- str|Flask.Response Renders either the login template or redirects to the main index. """ form = LoginForm() if g.user: flash( f"Already signed in as {g.user.UserName}. If it is not your user, sign out and sign in with correct user, please.", "alert-info", ) return redirect(url_for("main.index")) if form.validate_on_submit(): username = form.username.data password = form.password.data # # error = None fuser = db.session.execute(db.select(User).filter_by(name=username)).scalar() current_app.logger.debug(f"login : {fuser}") if fuser is not None and check_password_hash(fuser.password, password) and fuser.is_active: dbt.update_session_metadata(db, fuser, IP_address=request.remote_addr, login=True) login_session.clear() login_session["user_id"] = fuser.id # login_session["active"] = datetime.datetime.now() login_session.permanent = form.remember.data login_session["session_salt"] = str(fuser.id) + str(datetime.datetime.now()) login_session["urls"] = [] current_app.logger.info(f" login : Signed in {fuser} (IP: {str(request.remote_addr)})") if fuser.login_count < 2: flash("Please change your password on first login", "alert-info") return redirect(url_for("conf.changepasswd")) return redirect(url_for("main.index")) flash("Invalid username or password", "alert-warning") current_app.logger.warning( f" login : Failed sign in for {username} IP: {str(request.remote_addr)}" ) return render_template("auth/login.html", form=form)
[docs]@bp.before_app_request def load_logged_in_user(): """ Before request function. Loads the user data from the session cookie before a request. Will be needed for the user logic. Returns ------- None. """ user_id = login_session.get("user_id") if user_id is not None: # pylint: disable=assigning-non-slot g.user = db.session.get(User, int(user_id)) # This is correct. Pylint gives false error. g.search_form = SearchForm() g.salt = login_session.get("session_salt") g.ess_for_all = current_app.config["ESS_PER_GROUP"] else: g.user = None # pylint: disable=assigning-non-slot
[docs]@bp.route("/logout") def logout() -> Response: """ Logout current user. Logs user out and closes the session (remove session cookie). Returns ------- Flask.Response Redirect to log-in view . """ if g.user is None: flash("Session expired or session cookie deleted", "alert-warning") login_session.clear() return redirect(url_for("auth.login")) # dbt.update_session_metadata(db, g.user, login=False) current_app.logger.info(f" logout : Signed out {g.user}") login_session.clear() flash(f"Signed out user {g.user.name}", "alert-success") return redirect(url_for("auth.login"))
[docs]def login_required(view: Callable) -> Callable: """ Check login decorator. Decorator function that checks, if user is logged in. We can also use this later, if we create an admin group to give rights to groups or persons. Parameters ---------- view : function View or function to be decorated. Returns ------- Callable|Flask.Response Returns decorated function or redirects to log-in screen. """ @functools.wraps(view) def wrapped_view(**kwargs) -> Callable | Response: if g.user is None: flash("You need to login", "alert-secondary") current_app.logger.warning( f" login_required: Someone tried to access {view.__name__}" + f" without log in (IP: {str(request.remote_addr)})" ) return redirect(url_for("auth.login")) # Stop checking session expiring. Reset max session time in config. # if g.inactive > current_app.config["SESSION_LIFETIME"]: # current_app.logger.info( # f" : auth.load_logged_user : Signed out {g.user} as session expired)" # ) # # login_session["active"] = now # return redirect(url_for("auth.logout")) return view(**kwargs) return wrapped_view
[docs]def role_required(roles: list[str]) -> Callable: # That guy is quasi not to type in its current form. """ Check rights decorator. Decorator function that checks, if user is in the group to carry out a certain task. Parameters ---------- roles : List List of roles that can access the view. Returns ------- Callable|Flask.Response The function the role_required decorator is applied to or redirect to log-in view. """ def inner_decorator(view: Callable) -> Response | Callable: @functools.wraps(view) def wrapped_view(**kwargs) -> Response | Callable: if g.user is None: flash("You need to login", "alert-secondary") current_app.logger.warning( f" role_required: Someone tried to access {view.__name__}" + f" without log in (IP: {str(request.remote_addr)})" ) return redirect(url_for("auth.login")) # if g.inactive > current_app.config["SESSION_LIFETIME"]: # current_app.logger.info( # f" : auth.load_logged_user : Signed out {g.user} as session expired)" # ) # return redirect(url_for("auth.logout")) if g.user.role_member.name not in roles: flash("Your do not having the right role for this", "alert-warning") return abort( 403, description=f" : auth.role_required : {g.user} tried to access {view.__name__}" ) return view(**kwargs) return wrapped_view return inner_decorator