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 Flask
|
||||||
from flask import abort, flash, has_request_context, jsonify
|
from flask import abort, flash, has_request_context, jsonify
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
from flask.json import JSONEncoder
|
||||||
|
from flask.logging import default_handler
|
||||||
|
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flask_cas import CAS
|
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_moment import Moment
|
from flask_moment import Moment
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask.json import JSONEncoder
|
|
||||||
from flask.logging import default_handler
|
|
||||||
|
|
||||||
from jinja2 import select_autoescape
|
from jinja2 import select_autoescape
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
from flask_cas import CAS
|
||||||
|
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
ScoBugCatcher,
|
ScoBugCatcher,
|
||||||
ScoException,
|
ScoException,
|
||||||
ScoGenError,
|
ScoGenError,
|
||||||
|
ScoInvalidCSRF,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
APIInvalidParams,
|
APIInvalidParams,
|
||||||
)
|
)
|
||||||
@ -71,6 +74,15 @@ def handle_access_denied(exc):
|
|||||||
return render_template("error_access_denied.j2", exc=exc), 403
|
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):
|
def internal_server_error(exc):
|
||||||
"""Bugs scodoc, erreurs 500"""
|
"""Bugs scodoc, erreurs 500"""
|
||||||
# note that we set the 500 status explicitly
|
# 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(ScoGenError, handle_sco_value_error)
|
||||||
app.register_error_handler(ScoValueError, 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(ScoBugCatcher, handle_sco_bug)
|
||||||
|
app.register_error_handler(ScoInvalidCSRF, handle_invalid_csrf)
|
||||||
app.register_error_handler(AccessDenied, handle_access_denied)
|
app.register_error_handler(AccessDenied, handle_access_denied)
|
||||||
app.register_error_handler(500, internal_server_error)
|
app.register_error_handler(500, internal_server_error)
|
||||||
app.register_error_handler(503, postgresql_server_error)
|
app.register_error_handler(503, postgresql_server_error)
|
||||||
|
@ -5,12 +5,13 @@
|
|||||||
import http
|
import http
|
||||||
|
|
||||||
import flask
|
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
|
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||||
import flask_login
|
import flask_login
|
||||||
|
|
||||||
from app import login
|
from app import login
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
basic_auth = HTTPBasicAuth()
|
basic_auth = HTTPBasicAuth()
|
||||||
@ -84,3 +85,15 @@ def unauthorized():
|
|||||||
if request.blueprint == "api" or request.blueprint == "apiweb":
|
if request.blueprint == "api" or request.blueprint == "apiweb":
|
||||||
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
|
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
|
||||||
return redirect(url_for("auth.login"))
|
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
|
import flask
|
||||||
from flask import current_app, flash, render_template
|
from flask import current_app, flash, render_template
|
||||||
from flask import redirect, url_for, request
|
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 sqlalchemy import func
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth import bp
|
from app.auth import bp, cas, logic
|
||||||
from app.auth import cas
|
|
||||||
from app.auth.forms import (
|
from app.auth.forms import (
|
||||||
CASUsersImportConfigForm,
|
CASUsersImportConfigForm,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
@ -87,14 +86,7 @@ def login_scodoc():
|
|||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout() -> flask.Response:
|
def logout() -> flask.Response:
|
||||||
"Logout a scodoc user. If CAS session, logout from CAS. Redirect."
|
"Logout a scodoc user. If CAS session, logout from CAS. Redirect."
|
||||||
if current_user:
|
return logic.logout()
|
||||||
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"))
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/create_user", methods=["GET", "POST"])
|
@bp.route("/create_user", methods=["GET", "POST"])
|
||||||
@ -140,7 +132,10 @@ def reset_password_request():
|
|||||||
)
|
)
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
return render_template(
|
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 html
|
||||||
import re
|
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
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
# re validant dd/mm/yyyy
|
# re validant dd/mm/yyyy
|
||||||
@ -22,7 +27,7 @@ def TrivialFormulator(
|
|||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription=(),
|
formdescription=(),
|
||||||
initvalues={},
|
initvalues=None,
|
||||||
method="post",
|
method="post",
|
||||||
enctype=None,
|
enctype=None,
|
||||||
submitlabel="OK",
|
submitlabel="OK",
|
||||||
@ -32,7 +37,7 @@ def TrivialFormulator(
|
|||||||
cssclass="",
|
cssclass="",
|
||||||
cancelbutton=None,
|
cancelbutton=None,
|
||||||
submitbutton=True,
|
submitbutton=True,
|
||||||
submitbuttonattributes=[],
|
submitbuttonattributes=None,
|
||||||
top_buttons=False, # place buttons at top of form
|
top_buttons=False, # place buttons at top of form
|
||||||
bottom_buttons=True, # buttons after form
|
bottom_buttons=True, # buttons after form
|
||||||
html_foot_markup="",
|
html_foot_markup="",
|
||||||
@ -99,7 +104,7 @@ def TrivialFormulator(
|
|||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription,
|
formdescription,
|
||||||
initvalues,
|
initvalues or {},
|
||||||
method,
|
method,
|
||||||
enctype,
|
enctype,
|
||||||
submitlabel,
|
submitlabel,
|
||||||
@ -109,7 +114,7 @@ def TrivialFormulator(
|
|||||||
cssclass=cssclass,
|
cssclass=cssclass,
|
||||||
cancelbutton=cancelbutton,
|
cancelbutton=cancelbutton,
|
||||||
submitbutton=submitbutton,
|
submitbutton=submitbutton,
|
||||||
submitbuttonattributes=submitbuttonattributes,
|
submitbuttonattributes=submitbuttonattributes or [],
|
||||||
top_buttons=top_buttons,
|
top_buttons=top_buttons,
|
||||||
bottom_buttons=bottom_buttons,
|
bottom_buttons=bottom_buttons,
|
||||||
html_foot_markup=html_foot_markup,
|
html_foot_markup=html_foot_markup,
|
||||||
@ -134,8 +139,8 @@ class TF(object):
|
|||||||
self,
|
self,
|
||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription=[],
|
formdescription=None,
|
||||||
initvalues={},
|
initvalues=None,
|
||||||
method="POST",
|
method="POST",
|
||||||
enctype=None,
|
enctype=None,
|
||||||
submitlabel="OK",
|
submitlabel="OK",
|
||||||
@ -145,7 +150,7 @@ class TF(object):
|
|||||||
cssclass="",
|
cssclass="",
|
||||||
cancelbutton=None,
|
cancelbutton=None,
|
||||||
submitbutton=True,
|
submitbutton=True,
|
||||||
submitbuttonattributes=[],
|
submitbuttonattributes=None,
|
||||||
top_buttons=False, # place buttons at top of form
|
top_buttons=False, # place buttons at top of form
|
||||||
bottom_buttons=True, # buttons after form
|
bottom_buttons=True, # buttons after form
|
||||||
html_foot_markup="", # html snippet put at the end, just after the table
|
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.form_url = form_url
|
||||||
self.values = values.copy()
|
self.values = values.copy()
|
||||||
self.formdescription = list(formdescription)
|
self.formdescription = list(formdescription or [])
|
||||||
self.initvalues = initvalues
|
self.initvalues = initvalues or {}
|
||||||
self.method = method
|
self.method = method
|
||||||
self.enctype = enctype
|
self.enctype = enctype
|
||||||
self.submitlabel = submitlabel
|
self.submitlabel = submitlabel
|
||||||
@ -171,7 +176,7 @@ class TF(object):
|
|||||||
self.cssclass = cssclass
|
self.cssclass = cssclass
|
||||||
self.cancelbutton = cancelbutton
|
self.cancelbutton = cancelbutton
|
||||||
self.submitbutton = submitbutton
|
self.submitbutton = submitbutton
|
||||||
self.submitbuttonattributes = submitbuttonattributes
|
self.submitbuttonattributes = submitbuttonattributes or []
|
||||||
self.top_buttons = top_buttons
|
self.top_buttons = top_buttons
|
||||||
self.bottom_buttons = bottom_buttons
|
self.bottom_buttons = bottom_buttons
|
||||||
self.html_foot_markup = html_foot_markup
|
self.html_foot_markup = html_foot_markup
|
||||||
@ -189,11 +194,26 @@ class TF(object):
|
|||||||
"true if form has been submitted"
|
"true if form has been submitted"
|
||||||
if self.is_submitted:
|
if self.is_submitted:
|
||||||
return True
|
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):
|
def canceled(self):
|
||||||
"true if form has been canceled"
|
"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):
|
def getform(self):
|
||||||
"return HTML form"
|
"return HTML form"
|
||||||
@ -447,7 +467,13 @@ class TF(object):
|
|||||||
self.form_attrs,
|
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:
|
if self.top_buttons:
|
||||||
R.append(buttons_markup + "<p></p>")
|
R.append(buttons_markup + "<p></p>")
|
||||||
R.append(self.before_table.format(title=self.title))
|
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(),
|
scu.get_request_args(),
|
||||||
descr,
|
descr,
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="POST",
|
|
||||||
submitlabel="Générer et archiver les documents",
|
submitlabel="Générer et archiver les documents",
|
||||||
name="tf",
|
name="tf",
|
||||||
formid="group_selector",
|
formid="group_selector",
|
||||||
|
@ -38,6 +38,10 @@ class InvalidNoteValue(ScoException):
|
|||||||
"Valeur note invalide. Usage interne saisie note."
|
"Valeur note invalide. Usage interne saisie note."
|
||||||
|
|
||||||
|
|
||||||
|
class ScoInvalidCSRF(ScoException):
|
||||||
|
"Erreur validation token CSRF"
|
||||||
|
|
||||||
|
|
||||||
class ScoValueError(ScoException):
|
class ScoValueError(ScoException):
|
||||||
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
|
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
|
||||||
# mal nommée: super classe de toutes les exceptions avec page
|
# mal nommée: super classe de toutes les exceptions avec page
|
||||||
|
@ -122,7 +122,6 @@ def formsemestre_custommenu_edit(formsemestre_id):
|
|||||||
descr,
|
descr,
|
||||||
initvalues=initvalues,
|
initvalues=initvalues,
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="GET",
|
|
||||||
submitlabel="Enregistrer",
|
submitlabel="Enregistrer",
|
||||||
name="tf",
|
name="tf",
|
||||||
)
|
)
|
||||||
|
@ -633,7 +633,6 @@ function chkbx_select(field_id, state) {
|
|||||||
descr,
|
descr,
|
||||||
initvalues,
|
initvalues,
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="post",
|
|
||||||
submitlabel="Modifier les inscriptions",
|
submitlabel="Modifier les inscriptions",
|
||||||
cssclass="inscription",
|
cssclass="inscription",
|
||||||
name="tf",
|
name="tf",
|
||||||
|
@ -1441,7 +1441,6 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
descr,
|
descr,
|
||||||
{},
|
{},
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="GET",
|
|
||||||
submitlabel="Créer et peupler les groupes",
|
submitlabel="Créer et peupler les groupes",
|
||||||
name="tf",
|
name="tf",
|
||||||
)
|
)
|
||||||
|
@ -186,7 +186,7 @@ def do_evaluation_listenotes(
|
|||||||
cancelbutton=None,
|
cancelbutton=None,
|
||||||
submitbutton=None,
|
submitbutton=None,
|
||||||
bottom_buttons=False,
|
bottom_buttons=False,
|
||||||
method="GET",
|
method="GET", # consultation
|
||||||
cssclass="noprint",
|
cssclass="noprint",
|
||||||
name="tf",
|
name="tf",
|
||||||
is_submitted=True, # toujours "soumis" (démarre avec liste complète)
|
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(),
|
scu.get_request_args(),
|
||||||
descr,
|
descr,
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="get",
|
|
||||||
submitlabel="Générer document",
|
submitlabel="Générer document",
|
||||||
name="tf",
|
name="tf",
|
||||||
formid="group_selector",
|
formid="group_selector",
|
||||||
@ -554,7 +553,6 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
|||||||
scu.get_request_args(),
|
scu.get_request_args(),
|
||||||
descr,
|
descr,
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
method="POST",
|
|
||||||
submitlabel="Générer document",
|
submitlabel="Générer document",
|
||||||
name="tf",
|
name="tf",
|
||||||
formid="group_selector",
|
formid="group_selector",
|
||||||
|
@ -9,4 +9,15 @@
|
|||||||
{{ wtf.quick_form(form) }}
|
{{ wtf.quick_form(form) }}
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
@ -1248,7 +1248,7 @@ def XMLgetBilletsEtud(etudid=False, code_nip=False):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list_billets", methods=["GET"])
|
@bp.route("/list_billets", methods=["GET", "POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@scodoc7func
|
@scodoc7func
|
||||||
@ -1271,7 +1271,6 @@ def list_billets():
|
|||||||
request.base_url,
|
request.base_url,
|
||||||
scu.get_request_args(),
|
scu.get_request_args(),
|
||||||
(("billet_id", {"input_type": "text", "title": "Numéro du billet :"}),),
|
(("billet_id", {"input_type": "text", "title": "Numéro du billet :"}),),
|
||||||
method="get",
|
|
||||||
submitbutton=False,
|
submitbutton=False,
|
||||||
)
|
)
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
|
@ -2916,7 +2916,10 @@ sco_publish(
|
|||||||
methods=["GET", "POST"],
|
methods=["GET", "POST"],
|
||||||
)
|
)
|
||||||
sco_publish(
|
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(
|
sco_publish(
|
||||||
"/feuille_preparation_jury",
|
"/feuille_preparation_jury",
|
||||||
|
Loading…
Reference in New Issue
Block a user