Update to a more flexible admin authentication

We do not rely on the ADMIN_PASSWORD being defined
or not to ask for admin permissions.
Instead we now have a flexible admin auth that can
be conditionnaly bypassed
This commit is contained in:
0livd 2017-06-28 23:41:05 +02:00
parent 0e374cd5e0
commit d9ae72f4f3
7 changed files with 65 additions and 33 deletions

View file

@ -11,7 +11,11 @@ This document describes changes between each past release.
- **BREAKING CHANGE** Use a hashed ``ADMIN_PASSWORD`` instead of a clear text one, ``./budget/manage.py generate_password_hash`` can be used to generate a proper password HASH (#236) - **BREAKING CHANGE** Use a hashed ``ADMIN_PASSWORD`` instead of a clear text one, ``./budget/manage.py generate_password_hash`` can be used to generate a proper password HASH (#236)
- **BREAKING CHANGE** Turn the WSGI file into a python module, renamed from budget/ihatemoney.wsgi to budget/wsgi.py. Please update your Apache configuration! - **BREAKING CHANGE** Turn the WSGI file into a python module, renamed from budget/ihatemoney.wsgi to budget/wsgi.py. Please update your Apache configuration!
- Changed the recommended gunicorn configuration to use the wsgi module as an entrypoint - Changed the recommended gunicorn configuration to use the wsgi module as an entrypoint
- **BREAKING CHANGE** The default value of ``ADMIN_PASSWORD`` has changed. If you have a custom settings file which set ``ADMIN_PASSWORD`` to an empty string (""), the application will use the default admin password until you update your settings.
### Added
- Add a new setting to allow public project creation (ALLOW_PUBLIC_PROJECT_CREATION)
### Removed ### Removed

View file

@ -11,4 +11,6 @@ MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
ACTIVATE_DEMO_PROJECT = True ACTIVATE_DEMO_PROJECT = True
ADMIN_PASSWORD = "" ADMIN_PASSWORD = "pbkdf2:sha256:50000$jc3isZTD$b3be8d04ed5c2c1ac89d5eb777facc94adaee48d473c9620f1e0cb73f3dcfa11"
ALLOW_PUBLIC_PROJECT_CREATION = True

View file

@ -71,6 +71,19 @@ def configure():
UserWarning UserWarning
) )
if not app.config['ADMIN_PASSWORD']:
app.config['ADMIN_PASSWORD'] = default_settings.ADMIN_PASSWORD
# Since 2.0
warnings.warn(
"The way Ihatemoney handles admin authentication has changed. You seem to be using "
+ "an empty ADMIN_PASSWORD which is not supported anymore. Your ADMIN_PASWWORD has been"
+ " automatically set to the default password to let you access your admin endpoints."
+ " However this password is not secure and must be changed in your settings file. Use"
+ " the command './budget/manage.py generate_password_hash' to generate a proper"
+ " password hash and copy the output to the value of ADMIN_PASSWORD",
UserWarning
)
configure() configure()

View file

@ -28,9 +28,7 @@
</form> </form>
</div> </div>
<div class="col-xs-12 col-sm-5 col-md-3 offset-sm-1"> <div class="col-xs-12 col-sm-5 col-md-3 offset-sm-1">
{% if is_admin_mode_enabled %} {% if is_public_project_creation_allowed %}
<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"> <form id="creation-form" class="form-horizontal" action="{{ url_for(".create_project") }}" method="post">
<fieldset class="form-group"> <fieldset class="form-group">
<legend>...{{ _("or create a new one") }}</legend> <legend>...{{ _("or create a new one") }}</legend>
@ -40,6 +38,8 @@
<button class="btn" type="submit">{{ _("let's get started") }}</button> <button class="btn" type="submit">{{ _("let's get started") }}</button>
</div> </div>
</form> </form>
{% else %}
<a href="{{ url_for(".create_project") }}">...{{ _("or create a new one") }}</a>
{% endif %} {% endif %}
</main> </main>
</div> </div>

View file

@ -378,6 +378,8 @@ class BudgetTestCase(TestCase):
def test_admin_authentication(self): def test_admin_authentication(self):
run.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass") run.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
# Disable public project creation so we have an admin endpoint to test
run.app.config['ALLOW_PUBLIC_PROJECT_CREATION'] = False
# test the redirection to the authentication page when trying to access admin endpoints # test the redirection to the authentication page when trying to access admin endpoints
resp = self.app.get("/create") resp = self.app.get("/create")

View file

@ -34,17 +34,26 @@ main = Blueprint("main", __name__)
mail = Mail() mail = Mail()
def requires_admin(f): def requires_admin(bypass=None):
"""Require admin permissions for @requires_admin decorated endpoints. """Require admin permissions for @requires_admin decorated endpoints.
Has no effect if ADMIN_PASSWORD is empty (default value) Has no effect if ADMIN_PASSWORD is empty (default value)
The bypass variable is optionnal and used to conditionnaly bypass the admin authentication
It expects a tuple containing the name of an application setting and its expected value
e.g. if you use @require_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
Admin authentication will be bypassed when ALLOW_PUBLIC_PROJECT_CREATION is set to True
""" """
def check_admin(f):
@wraps(f) @wraps(f)
def admin_auth(*args, **kws): def admin_auth(*args, **kws):
is_admin_auth_bypassed = False
if bypass is not None and current_app.config.get(bypass[0]) == bypass[1]:
is_admin_auth_bypassed = True
is_admin = session.get('is_admin') is_admin = session.get('is_admin')
if is_admin or not current_app.config['ADMIN_PASSWORD']: if is_admin or is_admin_auth_bypassed:
return f(*args, **kws) return f(*args, **kws)
raise Redirect303(url_for('.admin', goto=request.path)) raise Redirect303(url_for('.admin', goto=request.path))
return admin_auth return admin_auth
return check_admin
@main.url_defaults @main.url_defaults
@ -157,18 +166,17 @@ def authenticate(project_id=None):
def home(): def home():
project_form = ProjectForm() project_form = ProjectForm()
auth_form = AuthenticationForm() auth_form = AuthenticationForm()
# If ADMIN_PASSWORD is empty we consider that admin mode is disabled is_public_project_creation_allowed = current_app.config['ALLOW_PUBLIC_PROJECT_CREATION']
is_admin_mode_enabled = bool(current_app.config['ADMIN_PASSWORD'])
is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT'] is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
return render_template("home.html", project_form=project_form, return render_template("home.html", project_form=project_form,
is_demo_project_activated=is_demo_project_activated, is_demo_project_activated=is_demo_project_activated,
is_admin_mode_enabled=is_admin_mode_enabled, is_public_project_creation_allowed=is_public_project_creation_allowed,
auth_form=auth_form, session=session) auth_form=auth_form, session=session)
@main.route("/create", methods=["GET", "POST"]) @main.route("/create", methods=["GET", "POST"])
@requires_admin @requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
def create_project(): def create_project():
form = ProjectForm() form = ProjectForm()
if request.method == "GET" and 'project_id' in request.values: if request.method == "GET" and 'project_id' in request.values:

View file

@ -72,23 +72,26 @@ ihatemoney relies on a configuration file. If you run the application for the
first time, you will need to take a few moments to configure the application first time, you will need to take a few moments to configure the application
properly. properly.
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| Setting name | Default | What does it do? | | Setting name | Default | What does it do? |
+============================+===========================+========================================================================================+ +==============================+===========================+========================================================================================+
| SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information | | SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information |
| | | on the format used can be found on `the SQLAlchemy documentation`. | | | | on the format used can be found on `the SQLAlchemy documentation`. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. | | SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email adress to use when sending | | MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email adress to use when sending |
| | "budget@notmyidea.org")`` | emails. | | | "budget@notmyidea.org")`` | emails. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. | | ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| | ``""`` | If not empty, the specified password must be entered to create new projects. | | | | Hashed password to access protected endpoints. The default password is ``adminpass``. |
| ADMIN_PASSWORD | | To generate the proper password HASH, use ``./budget/manage.py generate_password_hash``| | ADMIN_PASSWORD | ``"pbkdf2:sha256:50.."`` | **This needs to be changed**. |
| | | To generate the proper password HASH, use ``./budget/manage.py generate_password_hash``|
| | | and copy its output into the value of *ADMIN_PASSWORD*. | | | | and copy its output into the value of *ADMIN_PASSWORD*. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+ +------------------------------+---------------------------+----------------------------------------------------------------------------------------+
| ALLOW_PUBLIC_PROJECT_CREATION| ``True`` | If set to `True`, everyone can create a project without entering the admin password |
+------------------------------+---------------------------+----------------------------------------------------------------------------------------+
.. _`the SQLAlechemy documentation`: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls .. _`the SQLAlechemy documentation`: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls