From f081ff72ad8ec7d06ed9c1937871a7662eec3f5e Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 3 May 2016 21:11:49 +0200 Subject: [PATCH 2/2] add new parameter anonymise to API /api/forms// endpoints returning anonymized formdata (#9146) Service is open to any request bearing a valid signature, without needing to to authenticate as a known user. It's also open to authenticated admin users for debugging. --- help/fr/api-get.page | 19 ++++++++++++++ tests/test_api.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ wcs/api.py | 24 ++++++++++------- 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/help/fr/api-get.page b/help/fr/api-get.page index e8ca31e..9ec8eb0 100644 --- a/help/fr/api-get.page +++ b/help/fr/api-get.page @@ -294,5 +294,24 @@ champs de type « Fichier » ne sont pas exportés. +
+Données anonymisées + +

+Les APIs « List de formulaires » et le mode Pull de récupération d'un formulaire accepte un +paramètre supplémentaire anonymise. Quand celui-ci est présent des données anonymisées +des formulaires sont renvoyées et les contrôles d'accès sont simplifiés à une signature simple, il +n'est pas nécessaire de préciser l'identifiant d'un utilisateur. +

+ + +$ curl -H "Accept: application/json" \ + https://www.example.net/api/forms/inscriptions/list?full=on&anonymise +$ curl -H "Accept: application/json" \ + https://www.example.net/api/forms/inscriptions/10/?anonymise + + +

+ diff --git a/tests/test_api.py b/tests/test_api.py index 5acd43e..3dc3efa 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -898,6 +898,7 @@ def test_api_list_formdata(pub, local_user): upload = PicklableUpload('test.txt', 'text/plain', 'ascii') upload.receive(['base64me']) formdata.data = {'0': 'FOO BAR %d' % i, '2': upload} + formdata.user_id = local_user.id if i%4 == 0: formdata.data['1'] = 'foo' formdata.data['1_display'] = 'foo' @@ -938,6 +939,7 @@ def test_api_list_formdata(pub, local_user): assert 'receipt_time' in resp.json[0] assert 'fields' in resp.json[0] assert 'file' not in resp.json[0]['fields'] # no file export in full lists + assert 'user' in resp.json[0] assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['backoffice'] is True assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['channel'] == 'Mail' @@ -960,6 +962,78 @@ def test_api_list_formdata(pub, local_user): resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter=all', user=local_user)) assert len(resp.json) == 30 +def test_api_anonymized_formdata(pub, local_user): + Role.wipe() + role = Role(name='test') + role.store() + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'test' + formdef.workflow_roles = {'_receiver': role.id} + formdef.fields = [ + fields.StringField(id='0', label='foobar', varname='foobar'), + fields.ItemField(id='1', label='foobar3', varname='foobar3', type='item', + items=['foo', 'bar', 'baz']), + fields.FileField(id='2', label='foobar4', varname='file'), + ] + formdef.store() + + data_class = formdef.data_class() + data_class.wipe() + + for i in range(30): + formdata = data_class() + date = time.strptime('2014-01-20', '%Y-%m-%d') + upload = PicklableUpload('test.txt', 'text/plain', 'ascii') + upload.receive(['base64me']) + formdata.data = {'0': 'FOO BAR %d' % i, '2': upload} + formdata.user_id = local_user.id + if i%4 == 0: + formdata.data['1'] = 'foo' + formdata.data['1_display'] = 'foo' + elif i%4 == 1: + formdata.data['1'] = 'bar' + formdata.data['1_display'] = 'bar' + else: + formdata.data['1'] = 'baz' + formdata.data['1_display'] = 'baz' + + formdata.just_created() + if i%3 == 0: + formdata.jump_status('new') + else: + formdata.jump_status('finished') + formdata.store() + + # check access is granted even if the user has not the appropriate role + resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on', user=local_user)) + assert len(resp.json) == 30 + assert 'receipt_time' in resp.json[0] + assert 'fields' in resp.json[0] + assert 'user' not in resp.json[0] + assert 'file' not in resp.json[0]['fields'] # no file export in full lists + assert 'foobar3' in resp.json[0]['fields'] + assert 'foobar' not in resp.json[0]['fields'] + + # check access is granted event if there is no user + resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on')) + assert len(resp.json) == 30 + assert 'receipt_time' in resp.json[0] + assert 'fields' in resp.json[0] + assert 'user' not in resp.json[0] + assert 'file' not in resp.json[0]['fields'] # no file export in full lists + assert 'foobar3' in resp.json[0]['fields'] + assert 'foobar' not in resp.json[0]['fields'] + # check anonymise is enforced on detail view + resp = get_app(pub).get(sign_uri('/api/forms/%s/?anonymise&full=on' % resp.json[0]['id'])) + assert 'receipt_time' in resp.json + assert 'fields' in resp.json + assert 'user' not in resp.json + assert 'file' not in resp.json['fields'] # no file export in detail + assert 'foobar3' in resp.json['fields'] + assert 'foobar' not in resp.json['fields'] + def test_roles(pub, local_user): Role.wipe() role = Role(name='Hello World') diff --git a/wcs/api.py b/wcs/api.py index cbc1cfa..6108dd2 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -52,7 +52,7 @@ class ApiFormdataPage(FormStatusPage): class ApiFormPage(BackofficeFormPage): - _q_exports = [('list', 'json')] # same as backoffice but restricted to json export + _q_exports = [('list', 'json')] # restrict to API endpoints def __init__(self, component): try: @@ -63,14 +63,19 @@ class ApiFormPage(BackofficeFormPage): # otherwise be accessible if the user is the submitter. self.check_access() + def check_access(self): - api_user = get_user_from_api_query_string() - if not api_user: - if get_request().user and get_request().user.is_admin: - return # grant access to admins, to ease debug - raise AccessForbiddenError('user not authenticated') - if not self.formdef.is_of_concern_for_user(api_user): - raise AccessForbiddenError('unsufficient roles') + 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() + if not api_user: + if get_request().user and get_request().user.is_admin: + return # grant access to admins, to ease debug + raise AccessForbiddenError('user not authenticated') + if not self.formdef.is_of_concern_for_user(api_user): + raise AccessForbiddenError('unsufficient roles') def _q_lookup(self, component): try: @@ -82,8 +87,7 @@ class ApiFormPage(BackofficeFormPage): class ApiFormsDirectory(Directory): def _q_lookup(self, component): - api_user = get_user_from_api_query_string() - if not api_user: + 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') -- 2.1.4