Files
Pivot/rechten.py
T
2026-10-14 12:29:29 +02:00

475 lines
23 KiB
Python

"""
Rechtensysteem: wie mag wat, in welke school.
Gebruikers staan in de CENTRALE database (één account voor alle scholen).
Per gebruiker wordt per school bijgehouden op welke onderdelen hij rechten heeft:
Gebruiker_rechten (Gebruiker_ID, School_ID, Onderdeel_ID)
Zo kan iemand in school A '2FA tijdelijk uitzetten' mogen en in school B niet.
Veiligheidsgroepen:
1 = Administrator : alle rechten in alle scholen, beheert gebruikers, rechten en scholen
2 = Gebruiker : enkel de rechten die per school zijn aangevinkt
5 = Leerkracht : (geen account) personeelslid dat met Google inlogt → enkel de personeelsmodule
Gebruik in de code:
@recht_vereist('herstellingen') # route
if heeft_recht('tweestaps_uitzetten'): ... # in Python
{% if mag('co2') %} ... {% endif %} # in templates
"""
import functools
import sys
from collections import OrderedDict
from flask import session, g, flash, redirect, url_for, request, has_request_context
ADMIN = 1
GEBRUIKER = 2
VEILIGHEIDSGROEPEN = OrderedDict([
(ADMIN, 'Administrator'),
(GEBRUIKER, 'Gebruiker'),
])
# (code, naam, categorie). De volgorde hier is de volgorde op het scherm.
# Nieuwe onderdelen worden bij het opstarten automatisch in de tabel Onderdelen gezet.
ONDERDELEN = [
# --- Chromebooks ---
('eigendom_bekijken', 'Eigendom bekijken', 'Chromebooks'),
('toestellen_uitdelen', 'Toestellen uitdelen en inleveren', 'Chromebooks'),
('toestellen_beheren', 'Toestelleninventaris beheren', 'Chromebooks'),
('toestel_blokkeren', 'Chromebook blokkeren / deblokkeren', 'Chromebooks'),
('herstellingen', 'Herstellingen', 'Chromebooks'),
('signpost', 'Signpost-toestellen', 'Chromebooks'),
# --- Leerlingen ---
('leerlingen_bekijken', 'Leerlingen bekijken en bewerken', 'Leerlingen'),
('leerlingen_synchroniseren', 'Leerlingen toevoegen en synchroniseren', 'Leerlingen'),
('leerlingenaantal', 'Leerlingenaantallen (alle scholen)', 'Leerlingen'),
# --- Basisonderwijs (blok enkel zichtbaar bij een basisschool) ---
('bingel', 'Bingel: leerlingenlijst (Excel)', 'Basisonderwijs'),
# --- Internaat ---
('internen', 'Internaat: internen bekijken', 'Internaat'),
('internaat_aanwezigheid', 'Internaat: aanwezigheden registreren', 'Internaat'),
# --- Leerlingfacturatie ---
('leerlingfacturatie', 'Leerlingfacturatie: facturen bekijken', 'Leerlingfacturatie'),
# --- Google ---
('google_wachtwoord', 'Google-wachtwoord resetten', 'Google'),
('google_accounts', 'Google-accounts aanmaken', 'Google'),
('tweestaps_codes', '2FA: back-upcodes tonen / aanmaken', 'Google'),
('tweestaps_uitzetten', '2FA tijdelijk uitzetten (uitzondering)', 'Google'),
('google_licenties', 'Google-licenties', 'Google'),
('gmail_delegatie', 'Mailbox-delegatie', 'Google'),
('verzendlijsten', 'Verzendlijsten (Google Groepen) beheren', 'Google'),
('gmail_labels', 'E-mails met een label naar een andere mailbox', 'Google'),
('gmail_handtekeningen', 'Gmail-handtekeningen instellen (functie/telefoon)', 'Google'),
('gedeelde_drives', 'Gedeelde drives: leden en rechten beheren', 'Google'),
('gmail_contacten', 'Contacten: contactlijsten in mailboxen, adresboek', 'Google'),
# --- Smartschool ---
('smartschool', 'Smartschool-wachtwoorden & info', 'Smartschool'),
('smartschool_activiteit', 'Activiteit co-accounts & berichten', 'Smartschool'),
('afwezigheden', 'Afwezigheden leerlingen (B-codes)', 'Smartschool'),
('laatkomers', 'Laatkomers', 'Smartschool'),
('skore', 'Skore-synchronisatie (starten en plannen)', 'Smartschool'),
('sgr_groepen', 'SGR groepen: subgroepen en leden bekijken', 'Smartschool'),
# --- Personeel ---
('personeel_directie', 'Personeel: aanvragen goedkeuren (directie)', 'Personeel'),
('personeel_secretariaat', 'Personeel: dagoverzicht (secretariaat)', 'Personeel'),
('gopro', 'GO! Pro: personeel en rollen', 'Personeel'),
('personeelsaantal', 'Personeelsaantallen (alle scholen)', 'Personeel'),
# --- Tools ---
('qr_codes', 'QR-code generator', 'Tools'),
# --- IoT ---
('co2', 'CO2-sensoren', 'IoT'),
# --- Scholengroep ---
# --- Beheer ---
('bugs_bekijken', 'Bugs bekijken', 'Beheer'),
('bugs_beheren', 'Status van bugs wijzigen', 'Beheer'),
('nieuw_schooljaar', 'Nieuw schooljaar (accounts & toestellen)', 'Beheer'),
('ip_whitelist', 'IP-adressen toestaan voor deze school', 'Beheer'),
# --- PIVOT ---
('gebruikersbeheer', 'Gebruikers & rechten van deze school', 'PIVOT'),
('koppelingen', 'Koppelingen van deze school', 'PIVOT'),
]
CODES = [o[0] for o in ONDERDELEN]
# =====================================================================
# Tabellen (centrale database)
# =====================================================================
SCHEMA = [
"""CREATE TABLE IF NOT EXISTS Veiligheidsgroepen (
Veiligheidsgroep TINYINT NOT NULL PRIMARY KEY,
Naam VARCHAR(50) NOT NULL,
Omschrijving VARCHAR(255) NULL
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Gebruikers (
Gebruiker_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Gebruikersnaam VARCHAR(100) NOT NULL,
Veiligheidsgroep TINYINT NOT NULL DEFAULT 2,
Passwoordhash VARCHAR(255) NULL,
Emailadres VARCHAR(255) NULL,
Last_login DATETIME NULL,
IP_laatste_login VARCHAR(45) NULL,
Laatste_school VARCHAR(32) NULL,
Actief TINYINT NOT NULL DEFAULT 1,
UNIQUE KEY uq_gebruikersnaam (Gebruikersnaam),
UNIQUE KEY uq_emailadres (Emailadres),
CONSTRAINT fk_gebruikers_groep FOREIGN KEY (Veiligheidsgroep)
REFERENCES Veiligheidsgroepen (Veiligheidsgroep)
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Onderdelen (
Onderdeel_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Code VARCHAR(50) NOT NULL,
Naam VARCHAR(255) NOT NULL,
Categorie VARCHAR(50) NOT NULL,
Volgorde INT NOT NULL DEFAULT 0,
UNIQUE KEY uq_onderdeel_code (Code)
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Gebruiker_rechten (
Gebruiker_ID INT NOT NULL,
School_ID INT NOT NULL,
Onderdeel_ID INT NOT NULL,
Toegekend_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Toegekend_door VARCHAR(100) NULL,
PRIMARY KEY (Gebruiker_ID, School_ID, Onderdeel_ID),
KEY idx_rechten_school (School_ID),
KEY idx_rechten_onderdeel (Onderdeel_ID),
CONSTRAINT fk_rechten_gebruiker FOREIGN KEY (Gebruiker_ID)
REFERENCES Gebruikers (Gebruiker_ID) ON DELETE CASCADE,
CONSTRAINT fk_rechten_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE CASCADE,
CONSTRAINT fk_rechten_onderdeel FOREIGN KEY (Onderdeel_ID)
REFERENCES Onderdelen (Onderdeel_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Handleidingen (
Handleiding_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Slug VARCHAR(100) NOT NULL,
Titel VARCHAR(200) NOT NULL,
Categorie VARCHAR(100) NULL,
Samenvatting VARCHAR(300) NULL,
Inhoud MEDIUMTEXT NULL,
Zichtbaar VARCHAR(10) NOT NULL DEFAULT 'iedereen',
Volgorde INT NOT NULL DEFAULT 0,
Gewijzigd_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Gewijzigd_door VARCHAR(100) NULL,
UNIQUE KEY uq_handleiding_slug (Slug)
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Handleidingen_versies (
Versie_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Handleiding_ID INT NOT NULL,
Titel VARCHAR(200) NOT NULL,
Inhoud MEDIUMTEXT NULL,
Opgeslagen_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Door VARCHAR(100) NULL,
KEY idx_versie_handleiding (Handleiding_ID, Opgeslagen_op),
CONSTRAINT fk_versie_handleiding FOREIGN KEY (Handleiding_ID)
REFERENCES Handleidingen (Handleiding_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Leerlingenaantal_tellingen (
Telling_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
School_ID INT NOT NULL,
Peildatum DATE NOT NULL,
Schooljaar VARCHAR(10) NOT NULL,
Aantal INT NOT NULL,
Detail TEXT NULL,
Opgevraagd_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Door VARCHAR(100) NULL,
UNIQUE KEY uq_telling (School_ID, Peildatum),
KEY idx_telling_datum (Peildatum),
CONSTRAINT fk_telling_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Personeelsaantal_tellingen (
Telling_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
School_ID INT NOT NULL,
Peildatum DATE NOT NULL,
Schooljaar VARCHAR(10) NOT NULL,
Aantal INT NOT NULL,
Detail TEXT NULL,
Opgevraagd_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Door VARCHAR(100) NULL,
UNIQUE KEY uq_ptelling (School_ID, Peildatum),
KEY idx_ptelling_datum (Peildatum),
CONSTRAINT fk_ptelling_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Personeelsaantal_huidig (
School_ID INT NOT NULL PRIMARY KEY,
Aantal INT NOT NULL,
Detail TEXT NULL,
Gesynchroniseerd DATETIME NOT NULL,
CONSTRAINT fk_phuidig_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS PIVOT_instellingen (
Sleutel VARCHAR(50) NOT NULL PRIMARY KEY,
Waarde VARCHAR(255) NULL,
Bijgewerkt DATETIME NULL,
Door VARCHAR(100) NULL
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS IP_blokkades (
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Reeks VARCHAR(64) NOT NULL,
Soort VARCHAR(10) NOT NULL DEFAULT 'blokkeren',
Opmerking VARCHAR(255) NULL,
Scholen VARCHAR(1000) NULL,
Aangemaakt DATETIME NOT NULL,
Door VARCHAR(100) NULL,
Gebruiker_ID INT NULL,
Aantal_geweigerd INT NOT NULL DEFAULT 0,
Laatst_geweigerd DATETIME NULL,
UNIQUE KEY uq_ip_reeks (Reeks, Soort)
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Koppeling_tests (
School_ID INT NOT NULL,
Onderdeel VARCHAR(50) NOT NULL,
Ok TINYINT NULL,
Detail VARCHAR(500) NULL,
Getest_op DATETIME NOT NULL,
PRIMARY KEY (School_ID, Onderdeel),
CONSTRAINT fk_ktest_school FOREIGN KEY (School_ID) REFERENCES Scholen (School_ID) ON DELETE CASCADE
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Logs (
Log_ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
Bezochte_url VARCHAR(2048) NOT NULL,
Gebruiker_ID INT NULL,
School_ID INT NULL,
Actie VARCHAR(255) NULL,
IP VARCHAR(45) NULL,
KEY idx_logs_tijd (Timestamp),
KEY idx_logs_gebruiker (Gebruiker_ID),
CONSTRAINT fk_logs_gebruiker FOREIGN KEY (Gebruiker_ID)
REFERENCES Gebruikers (Gebruiker_ID) ON DELETE SET NULL,
CONSTRAINT fk_logs_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE SET NULL
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Status (
Bugstatus_ID TINYINT NOT NULL PRIMARY KEY,
Omschrijving VARCHAR(50) NOT NULL,
UNIQUE KEY uq_status_omschrijving (Omschrijving)
) ENGINE=InnoDB""",
"""CREATE TABLE IF NOT EXISTS Bugs (
Bug_ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Timestamp DATETIME NOT NULL,
Omschrijving TEXT NOT NULL,
Foutcode VARCHAR(100) NULL,
Bugstatus_ID TINYINT NOT NULL DEFAULT 1,
Gebruiker_ID INT NULL,
Gebruikersnaam VARCHAR(255) NULL,
School_ID INT NULL,
Gemeld_op DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_bugs_status (Bugstatus_ID),
CONSTRAINT fk_bugs_status FOREIGN KEY (Bugstatus_ID) REFERENCES Status (Bugstatus_ID),
CONSTRAINT fk_bugs_gebruiker FOREIGN KEY (Gebruiker_ID)
REFERENCES Gebruikers (Gebruiker_ID) ON DELETE SET NULL,
CONSTRAINT fk_bugs_school FOREIGN KEY (School_ID)
REFERENCES Scholen (School_ID) ON DELETE SET NULL
) ENGINE=InnoDB""",
]
BUGSTATUS = [(1, 'Nieuw'), (2, 'In behandeling'), (3, 'Fixed')]
def ensure_schema():
"""Maakt de tabellen aan en zet de vaste lijsten (groepen, onderdelen, bugstatussen) goed."""
from database import central_query
for sql in SCHEMA:
central_query(sql, commit=True)
# Bestaande tabel Gebruikers van vóór "laatst actieve school" (het aanmeldscherm vraagt geen school meer)
if 'laatste_school' not in {r['Field'].lower() for r in central_query("SHOW COLUMNS FROM Gebruikers") or []}:
central_query("ALTER TABLE Gebruikers ADD COLUMN Laatste_school VARCHAR(32) NULL AFTER IP_laatste_login", commit=True)
for nr, naam in VEILIGHEIDSGROEPEN.items():
central_query("INSERT INTO Veiligheidsgroepen (Veiligheidsgroep, Naam) VALUES (%s, %s) "
"ON DUPLICATE KEY UPDATE Naam = VALUES(Naam)", (nr, naam), commit=True)
for i, (code, naam, cat) in enumerate(ONDERDELEN, start=1):
central_query("INSERT INTO Onderdelen (Code, Naam, Categorie, Volgorde) VALUES (%s, %s, %s, %s) "
"ON DUPLICATE KEY UPDATE Naam = VALUES(Naam), Categorie = VALUES(Categorie), "
"Volgorde = VALUES(Volgorde)", (code, naam, cat, i * 10), commit=True)
for nr, oms in BUGSTATUS:
central_query("INSERT INTO Status (Bugstatus_ID, Omschrijving) VALUES (%s, %s) "
"ON DUPLICATE KEY UPDATE Omschrijving = VALUES(Omschrijving)", (nr, oms), commit=True)
# Standaardhandleidingen die nog niet bestaan (daarna beheer je ze in PIVOT; verwijderen = verbergen)
from services.handleidingen_standaard import STANDAARD
bestaand = {r['Slug'] for r in central_query("SELECT Slug FROM Handleidingen")}
for i, h in enumerate(STANDAARD, start=1):
if h['slug'] not in bestaand:
central_query("INSERT IGNORE INTO Handleidingen (Slug, Titel, Categorie, Samenvatting, Inhoud, Volgorde, "
"Gewijzigd_door) VALUES (%s, %s, %s, %s, %s, %s, 'PIVOT')",
(h['slug'], h['titel'], h['categorie'], h['samenvatting'], h['inhoud'].strip(), i * 10),
commit=True)
# =====================================================================
# Gebruikers
# =====================================================================
def zoek_gebruiker(naam_of_email):
"""Actieve of inactieve gebruiker op gebruikersnaam of e-mailadres (hoofdletterongevoelig)."""
from database import central_query
w = (naam_of_email or '').strip()
if not w:
return None
rows = central_query("SELECT * FROM Gebruikers WHERE Gebruikersnaam = %s OR Emailadres = %s "
"ORDER BY Gebruikersnaam = %s DESC LIMIT 1", (w, w.lower(), w))
return rows[0] if rows else None
def gebruiker_op_id(gid):
from database import central_query
rows = central_query("SELECT * FROM Gebruikers WHERE Gebruiker_ID = %s", (gid,))
return rows[0] if rows else None
def registreer_login(gid, ip):
from database import central_query
try:
central_query("UPDATE Gebruikers SET Last_login = NOW(), IP_laatste_login = %s WHERE Gebruiker_ID = %s",
(ip, gid), commit=True)
except Exception as e:
sys.stderr.write(f"PIVOT: laatste login niet bewaard: {e}\n")
def bewaar_laatste_school(gid, code):
"""Onthoudt de school waarin de gebruiker het laatst werkte (die wordt bij de volgende aanmelding geladen)."""
from database import central_query
if not gid or not code:
return
try:
central_query("UPDATE Gebruikers SET Laatste_school = %s WHERE Gebruiker_ID = %s", (code, gid), commit=True)
except Exception as e:
sys.stderr.write(f"PIVOT: laatst actieve school niet bewaard: {e}\n")
def rechten_van(gid, school_id):
"""Set met codes van onderdelen waarop gebruiker `gid` rechten heeft in school `school_id`."""
from database import central_query
rows = central_query(
"SELECT o.Code FROM Gebruiker_rechten r JOIN Onderdelen o ON o.Onderdeel_ID = r.Onderdeel_ID "
"WHERE r.Gebruiker_ID = %s AND r.School_ID = %s", (gid, school_id))
return {r['Code'] for r in rows}
def scholen_met_rechten(gid):
"""School_ID's waar de gebruiker minstens één recht heeft."""
from database import central_query
rows = central_query("SELECT DISTINCT School_ID FROM Gebruiker_rechten WHERE Gebruiker_ID = %s", (gid,))
return {r['School_ID'] for r in rows}
def mag_in_school(gebruiker, school):
"""Mag deze gebruiker (rij uit Gebruikers) in deze school inloggen?"""
if not gebruiker or not school:
return False
if int(gebruiker['Veiligheidsgroep']) == ADMIN:
return True
return school.id in scholen_met_rechten(gebruiker['Gebruiker_ID'])
def zet_rechten(gid, school_id, codes, door=None):
"""Vervangt alle rechten van gebruiker `gid` in school `school_id` door `codes`."""
from database import get_central_connection
codes = [c for c in codes if c in CODES]
conn = get_central_connection()
cur = conn.cursor()
try:
conn.start_transaction()
cur.execute("DELETE FROM Gebruiker_rechten WHERE Gebruiker_ID = %s AND School_ID = %s", (gid, school_id))
if codes:
plek = ','.join(['%s'] * len(codes))
cur.execute(
f"INSERT INTO Gebruiker_rechten (Gebruiker_ID, School_ID, Onderdeel_ID, Toegekend_door) "
f"SELECT %s, %s, Onderdeel_ID, %s FROM Onderdelen WHERE Code IN ({plek})",
(gid, school_id, door, *codes))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cur.close()
conn.close()
# =====================================================================
# Huidige gebruiker
# =====================================================================
def is_admin():
return bool(session.get('admin')) or bool(session.get('superbeheerder'))
def huidige_rechten():
"""Rechten van de ingelogde gebruiker in de huidige school (één keer per request opgehaald)."""
if not has_request_context() or not session.get('auth'):
return set()
if is_admin():
return set(CODES)
if '_rechten' in g:
return g._rechten
gid = session.get('gebruiker_id')
school = g.get('school')
rechten = set()
if gid and school:
try:
rechten = rechten_van(gid, school.id)
except Exception as e:
sys.stderr.write(f"PIVOT: rechten ophalen mislukt: {e}\n")
g._rechten = rechten
return rechten
def heeft_recht(*codes):
"""True als de ingelogde gebruiker in de huidige school minstens één van deze rechten heeft."""
rechten = huidige_rechten()
return any(c in rechten for c in codes)
def recht_vereist(*codes):
"""Decorator voor routes: de gebruiker moet in de huidige school één van deze rechten hebben."""
for c in codes:
if c not in CODES:
raise ValueError(f"Onbekend onderdeel '{c}'")
def decorator(view):
@functools.wraps(view)
def wrapped(*a, **kw):
if not session.get('auth') or not session.get('school'):
return redirect(url_for('auth.login'))
if not heeft_recht(*codes):
flash("Je hebt geen rechten voor dit onderdeel in deze school.", "error")
return redirect(url_for('main.dashboard'))
return view(*a, **kw)
return wrapped
return decorator
def admin_vereist(view):
@functools.wraps(view)
def wrapped(*a, **kw):
if not session.get('auth'):
return redirect(url_for('auth.login'))
if not is_admin():
flash("Enkel voor administrators.", "error")
return redirect(url_for('main.dashboard'))
return view(*a, **kw)
return wrapped
# =====================================================================
# Logboek (tabel Logs)
# =====================================================================
def log(actie=None, url=None):
"""Schrijft een regel in Logs. Fouten worden genegeerd (loggen mag PIVOT nooit blokkeren)."""
from database import central_query
try:
school = g.get('school') if has_request_context() else None
central_query(
"INSERT INTO Logs (Bezochte_url, Gebruiker_ID, School_ID, Actie, IP) VALUES (%s, %s, %s, %s, %s)",
((url or (request.full_path.rstrip('?') if has_request_context() else ''))[:2048],
session.get('gebruiker_id') if has_request_context() else None,
school.id if school else None,
(actie or '')[:255] or None,
request.remote_addr if has_request_context() else None),
commit=True)
except Exception as e:
sys.stderr.write(f"PIVOT: loggen mislukt: {e}\n")