Add more security and audit
This commit is contained in:
@@ -303,8 +303,7 @@ def add_scholengroep_ict():
|
||||
db.session.add(user)
|
||||
db.session.flush()
|
||||
audit_log('user.create', 'user', target_type='user', target_id=str(user.id),
|
||||
detail={'email': email, 'role': user.role, 'school_id': school_id},
|
||||
school_id=school_id)
|
||||
detail={'email': email, 'role': user.role})
|
||||
db.session.commit()
|
||||
return jsonify({'user': user.to_dict()}), 201
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ from datetime import datetime
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
from models import User, SchoolYear, Assessment, School, Class, AuditLog
|
||||
from app import db
|
||||
from services.doelen import load_index, load_vak, is_valid_vak_id
|
||||
from services.audit import audit_log
|
||||
from functools import wraps
|
||||
from app import db, limiter
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_assessments():
|
||||
|
||||
@api_bp.route('/assessments', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit('120 per minute') # max 2 per seconde per gebruiker
|
||||
def save_assessment():
|
||||
data = request.get_json() or {}
|
||||
vak_id = (data.get('vak_id') or '').strip()
|
||||
@@ -81,6 +82,9 @@ def save_assessment():
|
||||
return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400
|
||||
if status not in ('groen', 'oranje', 'roze', ''):
|
||||
return jsonify({'error': 'Ongeldige status — gebruik groen, oranje, roze of leeg'}), 400
|
||||
# Sanitiseer input — voorkomt oversized data in DB
|
||||
if len(vak_id) > 100 or len(goal_id) > 50:
|
||||
return jsonify({'error': 'Ongeldige invoer'}), 400
|
||||
if not current_user.school_id:
|
||||
return jsonify({'error': 'Account is nog niet gekoppeld aan een school'}), 400
|
||||
|
||||
@@ -283,8 +287,8 @@ def get_audit_log():
|
||||
if not current_user.is_school_ict:
|
||||
return jsonify({'error': 'Geen toegang'}), 403
|
||||
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = int(request.args.get('per_page', 50))
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
per_page = min(100, max(1, int(request.args.get('per_page', 50)))) # max 100 per pagina
|
||||
category = request.args.get('category')
|
||||
search = request.args.get('search', '').strip()
|
||||
|
||||
|
||||
@@ -15,19 +15,33 @@ import os
|
||||
import secrets
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import requests
|
||||
from services.audit import audit_log
|
||||
from app import db, limiter
|
||||
from flask import (Blueprint, render_template, request, redirect,
|
||||
url_for, flash, jsonify, session, current_app)
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from models import User, School
|
||||
from app import db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
def _safe_next_url(next_url: str | None) -> str:
|
||||
"""Valideer dat de next-redirect intern blijft — voorkomt open redirect aanvallen."""
|
||||
if not next_url:
|
||||
return url_for('pages.dashboard')
|
||||
parsed = urlparse(next_url)
|
||||
# Weiger externe URLs (heeft netloc) of protocol-relative URLs
|
||||
if parsed.netloc or parsed.scheme:
|
||||
return url_for('pages.dashboard')
|
||||
# Zorg dat het pad begint met /
|
||||
if not next_url.startswith('/'):
|
||||
return url_for('pages.dashboard')
|
||||
return next_url
|
||||
|
||||
ENTRA_AUTHORITY = "https://login.microsoftonline.com/common"
|
||||
ENTRA_AUTH_URL = f"{ENTRA_AUTHORITY}/oauth2/v2.0/authorize"
|
||||
ENTRA_TOKEN_URL = f"{ENTRA_AUTHORITY}/oauth2/v2.0/token"
|
||||
@@ -117,6 +131,7 @@ def logout():
|
||||
|
||||
|
||||
@auth_bp.route('/microsoft')
|
||||
@limiter.limit('20 per minute')
|
||||
def microsoft_login():
|
||||
if not _entra_client_id():
|
||||
flash('Microsoft login is niet geconfigureerd.', 'error')
|
||||
@@ -138,6 +153,7 @@ def microsoft_login():
|
||||
|
||||
|
||||
@auth_bp.route('/callback')
|
||||
@limiter.limit('20 per minute')
|
||||
def microsoft_callback():
|
||||
error = request.args.get('error')
|
||||
if error:
|
||||
@@ -220,10 +236,11 @@ def microsoft_callback():
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Entra login: {email} (nieuw: {is_new}, school_id: {user.school_id})")
|
||||
return redirect(request.args.get('next') or url_for('pages.dashboard'))
|
||||
return redirect(_safe_next_url(request.args.get('next')))
|
||||
|
||||
|
||||
@auth_bp.route('/setup', methods=['GET', 'POST'])
|
||||
@limiter.limit('5 per minute')
|
||||
def setup():
|
||||
admin = User.query.filter_by(role='superadmin').first()
|
||||
if admin and admin.password_hash:
|
||||
@@ -252,7 +269,7 @@ def setup():
|
||||
first_name='Super', last_name='Admin')
|
||||
db.session.add(admin)
|
||||
|
||||
admin.set_password(password)
|
||||
admin.set_password(password) # pbkdf2:sha256 — zie models.py voor hash methode
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
@@ -264,6 +281,7 @@ def setup():
|
||||
|
||||
|
||||
@auth_bp.route('/superadmin-login', methods=['POST'])
|
||||
@limiter.limit('10 per minute; 30 per hour')
|
||||
def superadmin_login():
|
||||
"""Fallback login ENKEL voor de superadmin — niet voor gewone gebruikers."""
|
||||
if current_user.is_authenticated:
|
||||
|
||||
Reference in New Issue
Block a user