mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
Public project creation and admin permissions (#210)
* Add a @requires_admin decorator It can be used to protect specific endpoints with ADMIN_PASSWORD (a password that is stored unencrypted in the settings) The decorator has no effect if ADMIN_PASSWORD is an empty string (default value) * Require admin permissions to access create project endpoint When ADMIN_PASSWORD is not empty, project creation form on the home page will be replaced by a link to the create project endpoint so one is able to enter the admin password before filling the form
This commit is contained in:
parent
091553be56
commit
ea8eda35a7
9 changed files with 93 additions and 2 deletions
|
@ -10,3 +10,5 @@ SECRET_KEY = "tralala"
|
|||
MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
|
||||
|
||||
ACTIVATE_DEMO_PROJECT = True
|
||||
|
||||
ADMIN_PASSWORD = ""
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
to") }} <a href="{{ url_for(".create_project", project_id=create_project) }}">{{ _("create it") }}</a>{{ _("?") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if admin_auth %}
|
||||
<form class="form-horizontal" method="POST" accept-charset="utf-8">
|
||||
{{ forms.admin(form) }}
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="form-horizontal" method="POST" accept-charset="utf-8">
|
||||
{{ forms.authenticate(form) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -28,6 +28,9 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="col-3 offset-md-1">
|
||||
{% if is_admin_mode_enabled %}
|
||||
<a href="{{ url_for(".create_project") }}">...{{ _("or create a new one") }}</a>
|
||||
{% else %}
|
||||
<form id="creation-form" class="form-horizontal" action="{{ url_for(".create_project") }}" method="post">
|
||||
<fieldset class="form-group">
|
||||
<legend>...{{ _("or create a new one") }}</legend>
|
||||
|
@ -37,6 +40,7 @@
|
|||
<button class="btn" type="submit">{{ _("let's get started") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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('<a href="/admin?goto=%2Fcreate">', resp.data.decode('utf-8'))
|
||||
|
||||
# test right password
|
||||
resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'})
|
||||
self.assertIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
|
||||
|
||||
# test wrong password
|
||||
resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
|
||||
self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
|
||||
|
||||
# test empty password
|
||||
resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': ''})
|
||||
self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
|
||||
|
||||
def test_manage_bills(self):
|
||||
self.post_project("raclette")
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue