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 %}
+
+{% else %}
+{% 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 @@
+ {% 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: