diff --git a/budget/default_settings.py b/budget/default_settings.py index 210b3f20..15fe9cdd 100644 --- a/budget/default_settings.py +++ b/budget/default_settings.py @@ -10,3 +10,5 @@ SECRET_KEY = "tralala" MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org") ACTIVATE_DEMO_PROJECT = True + +ADMIN_PASSWORD = "" diff --git a/budget/forms.py b/budget/forms.py index f4464751..06df7430 100644 --- a/budget/forms.py +++ b/budget/forms.py @@ -83,6 +83,11 @@ class AuthenticationForm(FlaskForm): submit = SubmitField(_("Get in")) +class AdminAuthenticationForm(FlaskForm): + admin_password = PasswordField(_("Admin password"), validators=[Required()]) + submit = SubmitField(_("Get in")) + + class PasswordReminder(FlaskForm): id = StringField(_("Project identifier"), validators=[Required()]) submit = SubmitField(_("Send me the code by email")) diff --git a/budget/templates/authenticate.html b/budget/templates/authenticate.html index 98914d09..f241c487 100644 --- a/budget/templates/authenticate.html +++ b/budget/templates/authenticate.html @@ -7,7 +7,13 @@ to") }} {{ _("create it") }}{{ _("?") }}

{% endif %} +{% if admin_auth %} +
+ {{ forms.admin(form) }} +
+{% else %}
{{ forms.authenticate(form) }}
+{% endif %} {% endblock %} diff --git a/budget/templates/forms.html b/budget/templates/forms.html index 01e54867..ffdd165b 100644 --- a/budget/templates/forms.html +++ b/budget/templates/forms.html @@ -45,6 +45,16 @@ {% endmacro %} +{% macro admin(form) %} + + {% include "display_errors.html" %} + + {{ form.hidden_tag() }} + {{ input(form.admin_password) }} + {{ submit(form.submit) }} + +{% endmacro %} + {% macro create_project(form, home=False) %} {% include "display_errors.html" %} diff --git a/budget/templates/home.html b/budget/templates/home.html index edbee61a..c7a9d1e8 100644 --- a/budget/templates/home.html +++ b/budget/templates/home.html @@ -28,6 +28,9 @@
+ {% if is_admin_mode_enabled %} + ...{{ _("or create a new one") }} + {% else %}
...{{ _("or create a new one") }} @@ -37,6 +40,7 @@
+ {% endif %} {% endblock %} diff --git a/budget/tests/tests.py b/budget/tests/tests.py index e18e9c32..a1cedfad 100644 --- a/budget/tests/tests.py +++ b/budget/tests/tests.py @@ -44,6 +44,8 @@ class TestCase(unittest.TestCase): # clean after testing models.db.session.remove() models.db.drop_all() + # reconfigure app with default settings + run.configure() def login(self, project, password=None, test_client=None): password = password or project @@ -373,6 +375,25 @@ class BudgetTestCase(TestCase): c.get("/exit") self.assertNotIn('raclette', session) + def test_admin_authentication(self): + run.app.config['ADMIN_PASSWORD'] = "pass" + + # test the redirection to the authentication page when trying to access admin endpoints + resp = self.app.get("/create") + self.assertIn('', resp.data.decode('utf-8')) + + # test right password + resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'}) + self.assertIn('/create', resp.data.decode('utf-8')) + + # test wrong password + resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'}) + self.assertNotIn('/create', resp.data.decode('utf-8')) + + # test empty password + resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': ''}) + self.assertNotIn('/create', resp.data.decode('utf-8')) + def test_manage_bills(self): self.post_project("raclette") diff --git a/budget/translations/fr/LC_MESSAGES/messages.mo b/budget/translations/fr/LC_MESSAGES/messages.mo index 1794c62c..c824b18a 100644 Binary files a/budget/translations/fr/LC_MESSAGES/messages.mo and b/budget/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/budget/translations/fr/LC_MESSAGES/messages.po b/budget/translations/fr/LC_MESSAGES/messages.po index 8bf347ab..609846ff 100644 --- a/budget/translations/fr/LC_MESSAGES/messages.po +++ b/budget/translations/fr/LC_MESSAGES/messages.po @@ -41,6 +41,10 @@ msgstr "Email" msgid "Project identifier" msgstr "Identifiant du projet" +#: forms.py:87 +msgid "Admin password" +msgstr "Mot de passe administrateur" + #: forms.py:87 templates/send_invites.html:5 msgid "Create the project" msgstr "Créer le projet" @@ -158,6 +162,10 @@ msgstr "remboursements" msgid "Export file format" msgstr "Format du fichier d'export" +#: web.py:95 +msgid "This admin password is not the right one" +msgstr "Le mot de passe administrateur que vous avez entré n'est pas correct" + #: web.py:95 msgid "This private code is not the right one" msgstr "Le code que vous avez entré n'est pas correct" diff --git a/budget/web.py b/budget/web.py index efb427cb..3bfa73a1 100644 --- a/budget/web.py +++ b/budget/web.py @@ -16,11 +16,12 @@ from flask_babel import get_locale, gettext as _ from smtplib import SMTPRecipientsRefused import werkzeug from sqlalchemy import orm +from functools import wraps # local modules from models import db, Project, Person, Bill -from forms import AuthenticationForm, EditProjectForm, InviteForm, \ - MemberForm, PasswordReminder, ProjectForm, get_billform_for, \ +from forms import AdminAuthenticationForm, AuthenticationForm, EditProjectForm, \ + InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for, \ ExportForm from utils import Redirect303, list_of_dicts2json, list_of_dicts2csv @@ -28,6 +29,19 @@ main = Blueprint("main", __name__) mail = Mail() +def requires_admin(f): + """Require admin permissions for @requires_admin decorated endpoints. + Has no effect if ADMIN_PASSWORD is empty (default value) + """ + @wraps(f) + def admin_auth(*args, **kws): + admin_password = session.get('admin_password', '') + if not admin_password == current_app.config['ADMIN_PASSWORD']: + raise Redirect303(url_for('.admin', goto=request.path)) + return f(*args, **kws) + return admin_auth + + @main.url_defaults def add_project_id(endpoint, values): """Add the project id to the url calls if it is expected. @@ -66,6 +80,23 @@ def pull_project(endpoint, values): url_for(".authenticate", project_id=project_id)) +@main.route("/admin", methods=["GET", "POST"]) +def admin(): + """Admin authentication""" + form = AdminAuthenticationForm() + goto = request.args.get('goto', url_for('.home')) + if request.method == "POST": + if form.validate(): + if form.admin_password.data == current_app.config['ADMIN_PASSWORD']: + session['admin_password'] = form.admin_password.data + session.update() + return redirect(goto) + else: + msg = _("This admin password is not the right one") + form.errors['admin_password'] = [msg] + return render_template("authenticate.html", form=form, admin_auth=True) + + @main.route("/authenticate", methods=["GET", "POST"]) def authenticate(project_id=None): """Authentication form""" @@ -121,14 +152,18 @@ def authenticate(project_id=None): def home(): project_form = ProjectForm() auth_form = AuthenticationForm() + # If ADMIN_PASSWORD is empty we consider that admin mode is disabled + is_admin_mode_enabled = bool(current_app.config['ADMIN_PASSWORD']) is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT'] return render_template("home.html", project_form=project_form, is_demo_project_activated=is_demo_project_activated, + is_admin_mode_enabled=is_admin_mode_enabled, auth_form=auth_form, session=session) @main.route("/create", methods=["GET", "POST"]) +@requires_admin def create_project(): form = ProjectForm() if request.method == "GET" and 'project_id' in request.values: