From 8ef7e7440699ded83ad14eb1627979814259b16f Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 26 Feb 2016 10:49:35 +0100 Subject: [PATCH 2/2] add new parameter anonymise to API /api/forms// endpoints returning anonymized formdata (#9146) Service is open to any request which bear a valid signature, no need 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 | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++- wcs/api.py | 24 +++++++++------- 3 files changed, 109 insertions(+), 11 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 b71f7f1..0a3b6ae 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -620,7 +620,8 @@ def test_formdata(pub, local_user): assert [x.get('id') for x in resp.json['roles']['_receiver']] == [str(role.id)] assert [x.get('id') for x in resp.json['roles']['_foobar']] == [str(another_role.id)] - assert [x.get('id') for x in resp.json['roles']['concerned']] == [str(role.id), str(another_role.id)] + assert (set([x.get('id') for x in resp.json['roles']['concerned']]) + == set([str(role.id), str(another_role.id)])) assert [x.get('id') for x in resp.json['roles']['actions']] == [str(role.id)] # check the ?format=json endpoint returns 403 @@ -732,6 +733,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' @@ -772,6 +774,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' @@ -794,6 +797,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 e538479..8a4f6be 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -159,7 +159,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: @@ -170,14 +170,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: @@ -189,8 +194,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