Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,326 @@
|
||||
from flask import Blueprint, render_template, request, session, redirect, url_for, flash, current_app, make_response
|
||||
from werkzeug.security import check_password_hash
|
||||
import time
|
||||
import functools
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from config import Config
|
||||
from tenancy import (alle_scholen, school_op_code, school_voor_email, scholen_voor_email, school_voor_host, in_school,
|
||||
huidige_school)
|
||||
import rechten
|
||||
from rechten import recht_vereist, ADMIN, GEBRUIKER
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
oauth = OAuth()
|
||||
|
||||
SCHOOL_COOKIE = 'pivot_school' # onthoudt de laatst gekozen school op dit toestel
|
||||
GOOGLE_METADATA = 'https://accounts.google.com/.well-known/openid-configuration'
|
||||
|
||||
|
||||
def _google_client(school=None):
|
||||
"""
|
||||
Google-login client: die van de school als die een eigen OAuth-client heeft,
|
||||
anders de gedeelde client uit config.ini. Geeft (naam, client) of (None, None).
|
||||
"""
|
||||
if school and school.heeft('GOOGLE_SSO_CLIENTID', 'GOOGLE_SSO_CLIENTSECRET'):
|
||||
naam = f"google_{school.code}_{''.join(c for c in school.versie if c.isdigit())}"
|
||||
cid, secret = school.get('GOOGLE_SSO_CLIENTID'), school.get('GOOGLE_SSO_CLIENTSECRET')
|
||||
elif Config.SSO_CLIENT_ID and Config.SSO_CLIENT_SECRET:
|
||||
naam, cid, secret = 'google', Config.SSO_CLIENT_ID, Config.SSO_CLIENT_SECRET
|
||||
else:
|
||||
return None, None
|
||||
client = oauth.create_client(naam)
|
||||
if client is None:
|
||||
client = oauth.register(name=naam, client_id=cid, client_secret=secret,
|
||||
server_metadata_url=GOOGLE_METADATA,
|
||||
client_kwargs={'scope': 'openid email profile'})
|
||||
return naam, client
|
||||
|
||||
|
||||
def gekozen_school():
|
||||
"""School die vastligt via de hostnaam, of anders de laatst gekozen school (cookie)."""
|
||||
return school_voor_host(request.host) or school_op_code(request.cookies.get(SCHOOL_COOKIE))
|
||||
|
||||
|
||||
def _nummer(school):
|
||||
"""Sorteersleutel: laagste officiële instellingsnummer eerst; scholen zonder nummer achteraan."""
|
||||
nr = ''.join(c for c in str(school.get('INSTELLINGSNUMMER') or '') if c.isdigit())
|
||||
return (0, int(nr), school.naam.lower()) if nr else (1, 0, school.naam.lower())
|
||||
|
||||
|
||||
def standaard_school(scholen=None):
|
||||
"""De school met het laagste instellingsnummer (voor wie nog nooit aangemeld was)."""
|
||||
scholen = alle_scholen() if scholen is None else list(scholen)
|
||||
return min(scholen, key=_nummer) if scholen else None
|
||||
|
||||
|
||||
def kies_school(gebruiker=None, toegang=None, ook=()):
|
||||
"""
|
||||
De school die na het aanmelden geladen wordt (het aanmeldscherm vraagt geen school meer):
|
||||
1) de laatst actieve school van de gebruiker,
|
||||
2) voor een nieuwe gebruiker: de school van zijn e-maildomein (+ eventueel de scholen in `ook`),
|
||||
3) de laatst gekozen school op dit toestel (cookie), 4) als laatste redmiddel: het laagste instellingsnummer.
|
||||
Telkens enkel scholen waarvoor toegang(school) True is (als `toegang` gegeven is).
|
||||
"""
|
||||
ok = toegang or (lambda s: True)
|
||||
domein = _volgens_domein((gebruiker or {}).get('Emailadres'))
|
||||
for s in [school_op_code((gebruiker or {}).get('Laatste_school')), *domein, *ook,
|
||||
school_op_code(request.cookies.get(SCHOOL_COOKIE))]:
|
||||
if s and ok(s):
|
||||
return s
|
||||
return standaard_school(s for s in alle_scholen() if ok(s))
|
||||
|
||||
|
||||
def _volgens_domein(email):
|
||||
"""Scholen met het e-maildomein (een domein kan door meerdere scholen gedeeld worden): de laatst gekozen
|
||||
school op dit toestel eerst, dan op instellingsnummer."""
|
||||
lijst = sorted(scholen_voor_email(email), key=_nummer)
|
||||
cookie = request.cookies.get(SCHOOL_COOKIE)
|
||||
return sorted(lijst, key=lambda s: s.code != cookie)
|
||||
|
||||
|
||||
def is_superbeheerder(email):
|
||||
return (email or '').lower() in Config.SUPERBEHEERDERS
|
||||
|
||||
|
||||
def _login_ok(school, user, naam, groep, superbeheerder=False, gebruiker=None):
|
||||
"""
|
||||
Zet de sessie. `gebruiker` is de rij uit de centrale tabel Gebruikers (of None voor een
|
||||
superbeheerder uit config.ini of een leerkracht zonder PIVOT-account).
|
||||
Administrators (Veiligheidsgroep 1) zijn superbeheerder: alle rechten in alle scholen.
|
||||
"""
|
||||
admin = bool(gebruiker and int(gebruiker['Veiligheidsgroep']) == ADMIN)
|
||||
session.clear()
|
||||
session["auth"] = True
|
||||
session["user"] = user
|
||||
session["naam"] = naam
|
||||
session["gebruiker_id"] = gebruiker['Gebruiker_ID'] if gebruiker else None
|
||||
session["admin"] = admin or bool(superbeheerder)
|
||||
session["superbeheerder"] = admin or bool(superbeheerder)
|
||||
if groep is None and gebruiker:
|
||||
groep = ADMIN if session["admin"] else GEBRUIKER
|
||||
session["security_group"] = int(groep) if groep is not None else None
|
||||
session["school"] = school.code if school else None
|
||||
if gebruiker:
|
||||
rechten.registreer_login(gebruiker['Gebruiker_ID'], request.remote_addr)
|
||||
if school:
|
||||
rechten.bewaar_laatste_school(gebruiker['Gebruiker_ID'], school.code)
|
||||
|
||||
|
||||
def _onthoud_school(response, school):
|
||||
if school:
|
||||
response.set_cookie(SCHOOL_COOKIE, school.code, max_age=365 * 24 * 3600, samesite='Lax',
|
||||
secure=request.is_secure, httponly=True)
|
||||
return response
|
||||
|
||||
|
||||
#----------------------------------------------
|
||||
# BRUTE FORCE BEVEILIGING
|
||||
#----------------------------------------------
|
||||
attempts = {}
|
||||
|
||||
def client_ip():
|
||||
return request.remote_addr
|
||||
|
||||
def is_locked():
|
||||
ip = client_ip()
|
||||
if ip in attempts and attempts[ip]['count'] >= 5:
|
||||
elapsed = time.time() - attempts[ip]['last_time']
|
||||
if elapsed < 60:
|
||||
return True, int(60 - elapsed)
|
||||
else:
|
||||
attempts.pop(ip)
|
||||
return False, 0
|
||||
|
||||
def record_failure():
|
||||
ip = client_ip()
|
||||
if ip not in attempts:
|
||||
attempts[ip] = {'count': 1, 'last_time': time.time()}
|
||||
else:
|
||||
attempts[ip]['count'] += 1
|
||||
attempts[ip]['last_time'] = time.time()
|
||||
|
||||
#----------------------------------------------
|
||||
# --- GOOGLE SSO ROUTES ---
|
||||
#----------------------------------------------
|
||||
|
||||
@auth_bp.route('/google-login')
|
||||
def google_login():
|
||||
"""Start het Google inlogproces. De school volgt uit de hostnaam, ?school=code of het e-maildomein."""
|
||||
vast = school_voor_host(request.host) or school_op_code(request.args.get('school'))
|
||||
# Zonder vaste school: de Google-client van de laatst gekozen school op dit toestel (of de standaardschool).
|
||||
# Welke school na het aanmelden geladen wordt, beslist de callback.
|
||||
school = vast or school_op_code(request.cookies.get(SCHOOL_COOKIE)) or standaard_school()
|
||||
naam, client = _google_client(school)
|
||||
if not client:
|
||||
flash("Inloggen met Google is (nog) niet ingesteld.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
session['sso'] = {'client': naam, 'school': vast.code if vast else ''}
|
||||
extra = {'prompt': 'select_account'}
|
||||
if vast and len(vast.domeinen) == 1:
|
||||
extra['hd'] = school.domein # Google toont enkel accounts van dit domein
|
||||
redirect_uri = url_for('auth.callback', _external=True)
|
||||
return client.authorize_redirect(redirect_uri, **extra)
|
||||
|
||||
|
||||
@auth_bp.route('/callback')
|
||||
def callback():
|
||||
"""Verwerkt het antwoord van Google en bepaalt de school op basis van het e-maildomein."""
|
||||
sso = session.pop('sso', None) or {}
|
||||
try:
|
||||
client = oauth.create_client(sso.get('client') or 'google')
|
||||
token = client.authorize_access_token()
|
||||
user_info = token.get('userinfo') or {}
|
||||
except Exception as e:
|
||||
flash("Fout bij ophalen van Google gegevens.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
email = (user_info.get('email') or '').lower()
|
||||
if not email or user_info.get('email_verified') is False:
|
||||
flash("Google gaf geen geverifieerd e-mailadres terug.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
naam = user_info.get('name') or email
|
||||
super_ = is_superbeheerder(email)
|
||||
|
||||
# Geregistreerde PIVOT-gebruiker (centrale tabel Gebruikers)?
|
||||
try:
|
||||
gebruiker = rechten.zoek_gebruiker(email)
|
||||
except Exception:
|
||||
flash("De centrale PIVOT-database is niet bereikbaar.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
if gebruiker and not gebruiker['Actief']:
|
||||
gebruiker = None
|
||||
|
||||
def heeft_toegang(s):
|
||||
return bool(gebruiker) and (super_ or rechten.mag_in_school(gebruiker, s))
|
||||
|
||||
gekozen = school_op_code(sso.get('school')) # enkel als de school vastligt (hostnaam)
|
||||
domeinscholen = _volgens_domein(email) # meerdere scholen kunnen hetzelfde domein hebben
|
||||
volgens_domein = next((s for s in domeinscholen if heeft_toegang(s)), None) or \
|
||||
(domeinscholen[0] if domeinscholen else None)
|
||||
if gekozen:
|
||||
school = gekozen
|
||||
elif gebruiker and heeft_toegang_ergens(gebruiker, super_):
|
||||
# Laatst actieve school → school van het e-maildomein → laatst gekozen op dit toestel → laagste nummer
|
||||
school = kies_school(gebruiker, heeft_toegang, ook=domeinscholen)
|
||||
elif super_:
|
||||
school = kies_school(None, None, ook=domeinscholen)
|
||||
else:
|
||||
school = volgens_domein
|
||||
|
||||
if gekozen and gekozen not in domeinscholen and not super_ and not heeft_toegang(gekozen):
|
||||
flash(f"{email} hoort niet bij {gekozen.naam}.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if school is None:
|
||||
if super_ or (gebruiker and int(gebruiker['Veiligheidsgroep']) == ADMIN):
|
||||
# Superbeheerder van wie het domein bij geen school hoort: meteen naar scholenbeheer
|
||||
_login_ok(None, email, naam, None, superbeheerder=True, gebruiker=gebruiker)
|
||||
flash("Ingelogd als superbeheerder. Kies of maak een school.", "success")
|
||||
return redirect(url_for('scholen.overzicht'))
|
||||
flash(f"Het domein van {email} hoort bij geen enkele school in PIVOT.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 1) PIVOT-gebruiker met rechten in deze school
|
||||
if heeft_toegang(school):
|
||||
_login_ok(school, gebruiker['Gebruikersnaam'], naam, None, super_, gebruiker=gebruiker)
|
||||
flash(f"Succesvol ingelogd via Google: {gebruiker['Gebruikersnaam']}", "success")
|
||||
return _onthoud_school(redirect(url_for("main.dashboard")), school)
|
||||
|
||||
# 2) Superbeheerder van de scholengroep (config.ini) → beheerder in elke school
|
||||
if super_:
|
||||
_login_ok(school, email, naam, ADMIN, True)
|
||||
return _onthoud_school(redirect(url_for("main.dashboard")), school)
|
||||
|
||||
# 3) Personeelslid van de school (personeels-OU's) → beperkte toegang tot de personeelsmodule.
|
||||
# Delen meerdere scholen het domein, dan de eerste school waar hij personeelslid is.
|
||||
from services.personeel_service import is_personeelslid, LEERKRACHT_GROEP
|
||||
for kandidaat in [school] + [s for s in domeinscholen if s is not school and not gekozen]:
|
||||
with in_school(kandidaat):
|
||||
if is_personeelslid(email):
|
||||
_login_ok(kandidaat, email, naam, LEERKRACHT_GROEP)
|
||||
return _onthoud_school(redirect(url_for("personeel.mijn")), kandidaat)
|
||||
|
||||
flash(f"Toegang geweigerd: {email} is niet geregistreerd in PIVOT van {school.naam}.", "error")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
def heeft_toegang_ergens(gebruiker, super_=False):
|
||||
return bool(gebruiker) and (super_ or any(rechten.mag_in_school(gebruiker, s) for s in alle_scholen()))
|
||||
|
||||
|
||||
#----------------------------------------------
|
||||
# --- HANDMATIGE LOGIN/LOGOUT ---
|
||||
#----------------------------------------------
|
||||
|
||||
def _login_pagina(school=None):
|
||||
vast = school_voor_host(request.host)
|
||||
return render_template("pin.html",
|
||||
scholen=alle_scholen(),
|
||||
gekozen=vast or school or gekozen_school(),
|
||||
school_vast=bool(vast),
|
||||
google_aan=bool(Config.SSO_CLIENT_ID) or any(
|
||||
s.heeft('GOOGLE_SSO_CLIENTID') for s in alle_scholen()))
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
locked, wait_time = is_locked()
|
||||
if locked:
|
||||
flash(f"Te veel pogingen. Probeer opnieuw over {wait_time} seconden.", "error")
|
||||
return _login_pagina()
|
||||
|
||||
if request.method == "POST":
|
||||
entered_username = request.form.get("username", "").strip()
|
||||
entered_password = request.form.get("password", "")
|
||||
school = school_voor_host(request.host) # ligt de school vast via de hostnaam?
|
||||
|
||||
if not entered_username or not entered_password:
|
||||
record_failure()
|
||||
flash("Vul zowel gebruikersnaam als wachtwoord in.", "error")
|
||||
return _login_pagina(school)
|
||||
|
||||
try:
|
||||
gebruiker = rechten.zoek_gebruiker(entered_username)
|
||||
except Exception:
|
||||
flash("De centrale PIVOT-database is niet bereikbaar.", "error")
|
||||
return _login_pagina(school)
|
||||
|
||||
if (gebruiker and gebruiker['Actief'] and gebruiker['Passwoordhash']
|
||||
and check_password_hash(gebruiker['Passwoordhash'], entered_password)):
|
||||
attempts.pop(client_ip(), None)
|
||||
if school and not rechten.mag_in_school(gebruiker, school):
|
||||
flash(f"Je hebt geen rechten in {school.naam}. Vraag een administrator om toegang.", "error")
|
||||
return _login_pagina(school)
|
||||
# Geen school gevraagd: laatst actieve school, anders de school van het e-maildomein
|
||||
school = school or kies_school(gebruiker, lambda s: rechten.mag_in_school(gebruiker, s))
|
||||
if not school:
|
||||
flash("Je hebt nog geen rechten in een school. Vraag een administrator om toegang.", "error")
|
||||
return _login_pagina()
|
||||
_login_ok(school, gebruiker['Gebruikersnaam'], gebruiker['Gebruikersnaam'], None, gebruiker=gebruiker)
|
||||
flash(f"Welkom terug, {gebruiker['Gebruikersnaam']}!", "success")
|
||||
return _onthoud_school(redirect(url_for("main.dashboard")), school)
|
||||
|
||||
record_failure()
|
||||
flash("Ongeldige inloggegevens.", "error")
|
||||
return _login_pagina(school)
|
||||
|
||||
return _login_pagina()
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash("Je bent nu uitgelogd.", "success")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
#----------------------------------------------
|
||||
# --- DECORATORS ---
|
||||
#----------------------------------------------
|
||||
|
||||
def login_required(view_func):
|
||||
@functools.wraps(view_func)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
if not session.get("auth") or not session.get("school"):
|
||||
return redirect(url_for("auth.login"))
|
||||
return view_func(*args, **kwargs)
|
||||
return wrapped_view
|
||||
|
||||
# Rechten per school: zie rechten.py (recht_vereist, heeft_recht, admin_vereist)
|
||||
Reference in new issue
Block a user