From b26edee7a8b45fb4b77c3231da30db7eb8bbbad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 26 Jul 2017 13:54:22 +0200 Subject: [PATCH] api: add possibility of http basic auth access to the ics endpoint (#16792) --- tests/test_api.py | 46 ++++++++++++++++++++++++++++++++++++++------ wcs/api.py | 14 +++++--------- wcs/api_utils.py | 18 ++++++++++++++--- wcs/backoffice/management.py | 5 ++--- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index b64310ec..abfda304 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1445,7 +1445,8 @@ def test_api_geojson_formdata(pub, local_user): formdef.store() resp = get_app(pub).get(sign_uri('/api/forms/test/geojson', user=local_user), status=404) -def test_api_ics_formdata(pub, local_user): +@pytest.fixture +def ics_data(local_user): Role.wipe() role = Role(name='test') role.store() @@ -1462,11 +1463,6 @@ def test_api_ics_formdata(pub, local_user): data_class = formdef.data_class() data_class.wipe() - # check access is denied if the user has not the appropriate role - resp = get_app(pub).get(sign_uri('/api/forms/test/ics/foobar', user=local_user), status=403) - # even if there's an anonymse parameter - resp = get_app(pub).get(sign_uri('/api/forms/test/ics/foobar?anonymise', user=local_user), status=403) - date = datetime.datetime(2014, 1, 20, 12, 00) for i in range(30): formdata = data_class() @@ -1479,6 +1475,14 @@ def test_api_ics_formdata(pub, local_user): formdata.jump_status('finished') formdata.store() +def test_api_ics_formdata(pub, local_user, ics_data): + role = Role.select()[0] + + # check access is denied if the user has not the appropriate role + resp = get_app(pub).get(sign_uri('/api/forms/test/ics/foobar', user=local_user), status=403) + # even if there's an anonymse parameter + resp = get_app(pub).get(sign_uri('/api/forms/test/ics/foobar?anonymise', user=local_user), status=403) + # add proper role to user local_user.roles = [role.id] local_user.store() @@ -1495,6 +1499,36 @@ def test_api_ics_formdata(pub, local_user): # check 404 on erroneous field var resp = get_app(pub).get(sign_uri('/api/forms/test/ics/xxx', user=local_user), status=404) +def test_api_ics_formdata_http_auth(pub, local_user, ics_data): + role = Role.select()[0] + + # no access + app = get_app(pub) + app.authorization = ('Basic', ('user', 'password')) + resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=403) + + # add authentication info + pub.load_site_options() + pub.site_options.add_section('api-http-auth-ics') + pub.site_options.set('api-http-auth-ics', 'user', 'password') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + + # check access is denied if the user has not the appropriate role + resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=403) + + # add proper role to user + local_user.roles = [role.id] + local_user.store() + + # check it gets the data + resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=200) + assert resp.headers['content-type'] == 'text/calendar; charset=utf-8' + assert resp.body.count('BEGIN:VEVENT') == 10 + + # check it fails with a different password + app.authorization = ('Basic', ('user', 'password2')) + resp = app.get('/api/forms/test/ics/foobar?email=%s' % local_user.email, status=403) + def test_roles(pub, local_user): Role.wipe() role = Role(name='Hello World') diff --git a/wcs/api.py b/wcs/api.py index 1fd35547..b00a1d40 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -117,16 +117,13 @@ class ApiFormPage(BackofficeFormPage): self.formdef = FormDef.get_by_urlname(component) except KeyError: raise TraversalError() - # check access for all paths, to block access to formdata that would - # otherwise be accessible if the user is the submitter. - self.check_access() - def check_access(self): + def check_access(self, api_name=None): if 'anonymise' in get_request().form: if not is_url_signed() or (get_request().user and get_request().user.is_admin): raise AccessForbiddenError('user not authenticated') else: - api_user = get_user_from_api_query_string() + api_user = get_user_from_api_query_string(api_name=api_name) if not api_user: if get_request().user and get_request().user.is_admin: return # grant access to admins, to ease debug @@ -138,6 +135,9 @@ class ApiFormPage(BackofficeFormPage): if component == 'ics': return self.ics() + # check access for all paths, to block access to formdata that would + # otherwise be accessible if the user is the submitter. + self.check_access() try: formdata = self.formdef.data_class().get(component) except KeyError: @@ -147,10 +147,6 @@ class ApiFormPage(BackofficeFormPage): class ApiFormsDirectory(Directory): def _q_lookup(self, component): - if not is_url_signed(): - # grant access to admins, to ease debug - if not (get_request().user and get_request().user.is_admin): - raise AccessForbiddenError('user not authenticated') return ApiFormPage(component) diff --git a/wcs/api_utils.py b/wcs/api_utils.py index 9e5a0e9a..516ca91f 100644 --- a/wcs/api_utils.py +++ b/wcs/api_utils.py @@ -98,10 +98,22 @@ def is_url_signed(utcnow=None, duration=DEFAULT_DURATION): return True -def get_user_from_api_query_string(): - if not is_url_signed(): +def get_user_from_api_query_string(api_name=None): + auth_header = get_request().get_header('Authorization', '') + if auth_header: + if not auth_header.startswith('Basic '): + # we do not handle other authentication schemes + raise AccessForbiddenError('unhandled authorization header') + auth_header = auth_header.split(' ', 1)[1] + username, password = base64.decodestring(auth_header).split(':', 1) + configured_password = get_publisher().get_site_option( + username, section='api-http-auth-%s' % api_name) + if configured_password != password: + raise AccessForbiddenError('invalid authorization') + elif not is_url_signed(): return None - # Signature is good. Now looking for the user, by email/NameID. + # Signature or auth header are ok. + # Look for the user, by email/NameID. user = None if get_request().form.get('email'): email = get_request().form.get('email') diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py index be75acef..b5e3b299 100644 --- a/wcs/backoffice/management.py +++ b/wcs/backoffice/management.py @@ -1604,7 +1604,8 @@ class FormPage(Directory): if 'anonymise' in get_request().form: # api/ will let this pass but we don't want that. raise errors.AccessForbiddenError() - self.check_access() + self.check_access('ics') + user = get_user_from_api_query_string('ics') or get_request().user formdef = self.formdef selected_filter = self.get_filter_from_query() @@ -1625,8 +1626,6 @@ class FormPage(Directory): else: raise errors.TraversalError() - user = get_user_from_api_query_string() or get_request().user - formdatas, total_count = FormDefUI(formdef).get_listing_items( selected_filter, user=user, query=query, criterias=criterias) -- 2.13.3