Anciens formulaires: ajout csrf

This commit is contained in:
Emmanuel Viennet 2023-03-19 10:26:03 +01:00
parent dc0c20b56d
commit 32c321bcd1
14 changed files with 97 additions and 39 deletions

View File

@ -17,25 +17,28 @@ from flask import current_app, g, request
from flask import Flask
from flask import abort, flash, has_request_context, jsonify
from flask import render_template
from flask.json import JSONEncoder
from flask.logging import default_handler
from flask_bootstrap import Bootstrap
from flask_caching import Cache
from flask_cas import CAS
from flask_login import LoginManager, current_user
from flask_mail import Mail
from flask_migrate import Migrate
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask.json import JSONEncoder
from flask.logging import default_handler
from jinja2 import select_autoescape
import sqlalchemy
from flask_cas import CAS
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoBugCatcher,
ScoException,
ScoGenError,
ScoInvalidCSRF,
ScoValueError,
APIInvalidParams,
)
@ -71,6 +74,15 @@ def handle_access_denied(exc):
return render_template("error_access_denied.j2", exc=exc), 403
def handle_invalid_csrf(exc):
"""Form submit with invalid CSRF token"""
# logout user and go back to login page with an error message
from app import auth
auth.logic.logout()
return render_template("error_csrf.j2", exc=exc), 404
def internal_server_error(exc):
"""Bugs scodoc, erreurs 500"""
# note that we set the 500 status explicitly
@ -260,6 +272,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(ScoBugCatcher, handle_sco_bug)
app.register_error_handler(ScoInvalidCSRF, handle_invalid_csrf)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error)

View File

@ -5,12 +5,13 @@
import http
import flask
from flask import g, redirect, request, url_for
from flask import current_app, g, redirect, request, url_for
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
import flask_login
from app import login
from app.auth.models import User
from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_utils import json_error
basic_auth = HTTPBasicAuth()
@ -84,3 +85,15 @@ def unauthorized():
if request.blueprint == "api" or request.blueprint == "apiweb":
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
return redirect(url_for("auth.login"))
def logout() -> flask.Response:
"""Logout the current user: If CAS session, logout from CAS. Redirect."""
if flask_login.current_user:
user_name = getattr(flask_login.current_user, "user_name", "anonymous")
current_app.logger.info(f"logout user {user_name}")
flask_login.logout_user()
if ScoDocSiteConfig.is_cas_enabled() and flask.session.get("scodoc_cas_login_date"):
flask.session.pop("scodoc_cas_login_date", None)
return redirect(url_for("cas.logout"))
return redirect(url_for("scodoc.index"))

View File

@ -6,12 +6,11 @@ auth.routes.py
import flask
from flask import current_app, flash, render_template
from flask import redirect, url_for, request
from flask_login import login_user, logout_user, current_user
from flask_login import login_user, current_user
from sqlalchemy import func
from app import db
from app.auth import bp
from app.auth import cas
from app.auth import bp, cas, logic
from app.auth.forms import (
CASUsersImportConfigForm,
LoginForm,
@ -87,14 +86,7 @@ def login_scodoc():
@bp.route("/logout")
def logout() -> flask.Response:
"Logout a scodoc user. If CAS session, logout from CAS. Redirect."
if current_user:
user_name = getattr(current_user, "user_name", "anonymous")
current_app.logger.info(f"logout user {user_name}")
logout_user()
if ScoDocSiteConfig.is_cas_enabled() and flask.session.get("scodoc_cas_login_date"):
flask.session.pop("scodoc_cas_login_date", None)
return redirect(url_for("cas.logout"))
return redirect(url_for("scodoc.index"))
return logic.logout()
@bp.route("/create_user", methods=["GET", "POST"])
@ -140,7 +132,10 @@ def reset_password_request():
)
return redirect(url_for("auth.login"))
return render_template(
"auth/reset_password_request.j2", title=_("Reset Password"), form=form
"auth/reset_password_request.j2",
title=_("Reset Password"),
form=form,
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
)

View File

@ -10,6 +10,11 @@
"""
import html
import re
import flask_wtf
import wtforms
from app import log
from app.scodoc.sco_exceptions import ScoInvalidCSRF
import app.scodoc.sco_utils as scu
# re validant dd/mm/yyyy
@ -22,7 +27,7 @@ def TrivialFormulator(
form_url,
values,
formdescription=(),
initvalues={},
initvalues=None,
method="post",
enctype=None,
submitlabel="OK",
@ -32,7 +37,7 @@ def TrivialFormulator(
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=[],
submitbuttonattributes=None,
top_buttons=False, # place buttons at top of form
bottom_buttons=True, # buttons after form
html_foot_markup="",
@ -99,7 +104,7 @@ def TrivialFormulator(
form_url,
values,
formdescription,
initvalues,
initvalues or {},
method,
enctype,
submitlabel,
@ -109,7 +114,7 @@ def TrivialFormulator(
cssclass=cssclass,
cancelbutton=cancelbutton,
submitbutton=submitbutton,
submitbuttonattributes=submitbuttonattributes,
submitbuttonattributes=submitbuttonattributes or [],
top_buttons=top_buttons,
bottom_buttons=bottom_buttons,
html_foot_markup=html_foot_markup,
@ -134,8 +139,8 @@ class TF(object):
self,
form_url,
values,
formdescription=[],
initvalues={},
formdescription=None,
initvalues=None,
method="POST",
enctype=None,
submitlabel="OK",
@ -145,7 +150,7 @@ class TF(object):
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=[],
submitbuttonattributes=None,
top_buttons=False, # place buttons at top of form
bottom_buttons=True, # buttons after form
html_foot_markup="", # html snippet put at the end, just after the table
@ -157,8 +162,8 @@ class TF(object):
):
self.form_url = form_url
self.values = values.copy()
self.formdescription = list(formdescription)
self.initvalues = initvalues
self.formdescription = list(formdescription or [])
self.initvalues = initvalues or {}
self.method = method
self.enctype = enctype
self.submitlabel = submitlabel
@ -171,7 +176,7 @@ class TF(object):
self.cssclass = cssclass
self.cancelbutton = cancelbutton
self.submitbutton = submitbutton
self.submitbuttonattributes = submitbuttonattributes
self.submitbuttonattributes = submitbuttonattributes or []
self.top_buttons = top_buttons
self.bottom_buttons = bottom_buttons
self.html_foot_markup = html_foot_markup
@ -189,11 +194,26 @@ class TF(object):
"true if form has been submitted"
if self.is_submitted:
return True
return self.values.get("%s_submitted" % self.formid, False)
form_submitted = self.values.get(f"{self.formid}_submitted", False)
if form_submitted:
self.check_csrf()
return form_submitted
def check_csrf(self):
"""check token for POST forms.
Raises ScoInvalidCSRF on failure.
"""
if self.method == "post":
token = self.values.get("csrf_token")
try:
flask_wtf.csrf.validate_csrf(token)
except wtforms.validators.ValidationError as exc:
log(f"Form.check_csrf: invalid CSRF token\n{exc.args}")
raise ScoInvalidCSRF() from exc
def canceled(self):
"true if form has been canceled"
return self.values.get("%s_cancel" % self.formid, False)
return self.values.get(f"{self.formid}_cancel", False)
def getform(self):
"return HTML form"
@ -447,7 +467,13 @@ class TF(object):
self.form_attrs,
)
)
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
if self.method == "post":
R.append(
f"""<input type="hidden" name="csrf_token" value="{
flask_wtf.csrf.generate_csrf()
}">"""
)
R.append(f"""<input type="hidden" name="{self.formid}_submitted" value="1">""")
if self.top_buttons:
R.append(buttons_markup + "<p></p>")
R.append(self.before_table.format(title=self.title))

View File

@ -500,7 +500,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="POST",
submitlabel="Générer et archiver les documents",
name="tf",
formid="group_selector",

View File

@ -38,6 +38,10 @@ class InvalidNoteValue(ScoException):
"Valeur note invalide. Usage interne saisie note."
class ScoInvalidCSRF(ScoException):
"Erreur validation token CSRF"
class ScoValueError(ScoException):
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
# mal nommée: super classe de toutes les exceptions avec page

View File

@ -122,7 +122,6 @@ def formsemestre_custommenu_edit(formsemestre_id):
descr,
initvalues=initvalues,
cancelbutton="Annuler",
method="GET",
submitlabel="Enregistrer",
name="tf",
)

View File

@ -633,7 +633,6 @@ function chkbx_select(field_id, state) {
descr,
initvalues,
cancelbutton="Annuler",
method="post",
submitlabel="Modifier les inscriptions",
cssclass="inscription",
name="tf",

View File

@ -1441,7 +1441,6 @@ def groups_auto_repartition(partition_id=None):
descr,
{},
cancelbutton="Annuler",
method="GET",
submitlabel="Créer et peupler les groupes",
name="tf",
)

View File

@ -186,7 +186,7 @@ def do_evaluation_listenotes(
cancelbutton=None,
submitbutton=None,
bottom_buttons=False,
method="GET",
method="GET", # consultation
cssclass="noprint",
name="tf",
is_submitted=True, # toujours "soumis" (démarre avec liste complète)

View File

@ -385,7 +385,6 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="get",
submitlabel="Générer document",
name="tf",
formid="group_selector",
@ -554,7 +553,6 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="POST",
submitlabel="Générer document",
name="tf",
formid="group_selector",

View File

@ -9,4 +9,15 @@
{{ wtf.quick_form(form) }}
</div>
</div>
{% if is_cas_enabled %}
<div style="margin-top: 12px; color: red;">
<p>Attention: ce mécanisme permet de changer le mot de passe ScoDoc
mais ne changera pas votre mot de passe sur le système de l'établissement.
</p>
<p>
Si vous vous connectez via vos identifiants de l'université (CAS), passez
par la procédure de celle-ci (ENT ou autre).
</p>
</div>
{% endif %}
{% endblock %}

View File

@ -1248,7 +1248,7 @@ def XMLgetBilletsEtud(etudid=False, code_nip=False):
return ""
@bp.route("/list_billets", methods=["GET"])
@bp.route("/list_billets", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
@ -1271,7 +1271,6 @@ def list_billets():
request.base_url,
scu.get_request_args(),
(("billet_id", {"input_type": "text", "title": "Numéro du billet :"}),),
method="get",
submitbutton=False,
)
if tf[0] == 0:

View File

@ -2916,7 +2916,10 @@ sco_publish(
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_pvjury_pdf", sco_pv_forms.formsemestre_pvjury_pdf, Permission.ScoView
"/formsemestre_pvjury_pdf",
sco_pv_forms.formsemestre_pvjury_pdf,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/feuille_preparation_jury",