diff --git a/app/__init__.py b/app/__init__.py index b28fc9adc..281cc747e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -169,7 +169,7 @@ def register_errorhandlers(application): error_code = getattr(error, 'code', 500) resp = make_response(render_template("error/{0}.html".format(error_code)), error_code) return useful_headers_after_request(resp) - for errcode in [401, 404, 500]: + for errcode in [401, 404, 403, 500]: application.errorhandler(errcode)(render_error) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 347d809df..cb15fce90 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -6,6 +6,8 @@ from app.main.dao import templates_dao from notifications_python_client.errors import HTTPError from app import job_api_client +from app.utils import user_has_permissions + @main.route("/services//dashboard") @login_required diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 34f936529..169b61bac 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -94,6 +94,7 @@ class User(UserMixin): self._email_address = fields.get('email_address') self._mobile_number = fields.get('mobile_number') self._password_changed_at = fields.get('password_changed_at') + self._permissions = set(fields.get('permissions')) if fields.get('permission') is not None else set() self._failed_login_count = 0 self._state = fields.get('state') self.max_failed_login_count = max_failed_login_count @@ -152,6 +153,25 @@ class User(UserMixin): def state(self, state): self._state = state + @property + def permissions(self): + return self._permissions + + @permissions.setter + def permissions(self, permissions): + if permissions is None: + permissions = set() + self._permissions = set(permissions) + + def add_permissions(self, permissions): + self._permissions.update(permissions) + + def remove_permissions(self, permissions): + self._permissions -= permissions + + def has_permissions(self, permissions): + return self._permissions > set(permissions) + @property def failed_login_count(self): return self._failed_login_count @@ -170,7 +190,8 @@ class User(UserMixin): "mobile_number": self.mobile_number, "password_changed_at": self.password_changed_at, "state": self.state, - "failed_login_count": self.failed_login_count} + "failed_login_count": self.failed_login_count, + "permissions": [x for x in self._permissions]} if getattr(self, '_password', None): dct['password'] = self._password return dct diff --git a/app/templates/error/403.html b/app/templates/error/403.html new file mode 100644 index 000000000..b52d58d8f --- /dev/null +++ b/app/templates/error/403.html @@ -0,0 +1,14 @@ +{% extends "withoutnav_template.html" %} +{% block page_title %}Page not found{% endblock %} +{% block maincolumn_content %} +
+
+

+ 403 +

+

+ You do not have permission to view this page. +

+
+
+{% endblock %} diff --git a/app/utils.py b/app/utils.py index 88a127a23..f814f4b81 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,3 +1,7 @@ +from functools import wraps +from flask import abort + + class BrowsableItem(object): """ Maps for the template browse-list. @@ -68,3 +72,16 @@ def format_phone_number(number): if len(number) < 9: raise InvalidPhoneError('Not enough digits') return '+447{}{}{}'.format(*re.findall('...', number)) + + +def user_has_permissions(*permissions): + def wrap(func): + @wraps(func) + def wrap_func(*args, **kwargs): + # We are making the assumption that the user is logged in. + from flask_login import current_user + if set(permissions) > set(current_user.permissions): + abort(403) + return func(*args, **kwargs) + return wrap_func + return wrap diff --git a/tests/app/main/test_utils.py b/tests/app/main/test_utils.py new file mode 100644 index 000000000..b95b0a272 --- /dev/null +++ b/tests/app/main/test_utils.py @@ -0,0 +1,23 @@ +import pytest +from flask import url_for + +from app.utils import user_has_permissions +from app.main.views.index import index +from werkzeug.exceptions import Forbidden + + +def test_user_has_permissions(app_, + api_user_active, + mock_get_user, + mock_get_user_by_email, + mock_login): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + decorator = user_has_permissions('something') + decorated_index = decorator(index) + try: + response = decorated_index() + pytest.fail("Failed to throw a forbidden exception") + except Forbidden: + pass