forked from ScoDoc/ScoDoc
Anciens formulaires: ajout csrf
This commit is contained in:
parent
dc0c20b56d
commit
32c321bcd1
@ -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)
|
||||
|
@ -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"))
|
||||
|
@ -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(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -122,7 +122,6 @@ def formsemestre_custommenu_edit(formsemestre_id):
|
||||
descr,
|
||||
initvalues=initvalues,
|
||||
cancelbutton="Annuler",
|
||||
method="GET",
|
||||
submitlabel="Enregistrer",
|
||||
name="tf",
|
||||
)
|
||||
|
@ -633,7 +633,6 @@ function chkbx_select(field_id, state) {
|
||||
descr,
|
||||
initvalues,
|
||||
cancelbutton="Annuler",
|
||||
method="post",
|
||||
submitlabel="Modifier les inscriptions",
|
||||
cssclass="inscription",
|
||||
name="tf",
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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 %}
|
@ -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:
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user