Projet

Général

Profil

0002-add-new-parameter-anonymise-to-API-api-forms-slug-en.patch

Benjamin Dauvergne, 03 mai 2016 21:54

Télécharger (7,84 ko)

Voir les différences:

Subject: [PATCH 2/2] add new parameter anonymise to API /api/forms/<slug>/
 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(-)
help/fr/api-get.page
294 294

  
295 295
</section>
296 296

  
297
<section>
298
<title>Données anonymisées</title>
299

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

  
307
<screen>
308
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
309
       https://www.example.net/api/forms/inscriptions/list?full=on&amp;anonymise</input>
310
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
311
       https://www.example.net/api/forms/inscriptions/10/?anonymise</input>
312
</screen>
313

  
314
</section>
315

  
297 316

  
298 317
</page>
tests/test_api.py
898 898
        upload = PicklableUpload('test.txt', 'text/plain', 'ascii')
899 899
        upload.receive(['base64me'])
900 900
        formdata.data = {'0': 'FOO BAR %d' % i, '2': upload}
901
        formdata.user_id = local_user.id
901 902
        if i%4 == 0:
902 903
            formdata.data['1'] = 'foo'
903 904
            formdata.data['1_display'] = 'foo'
......
938 939
    assert 'receipt_time' in resp.json[0]
939 940
    assert 'fields' in resp.json[0]
940 941
    assert 'file' not in resp.json[0]['fields'] # no file export in full lists
942
    assert 'user' in resp.json[0]
941 943

  
942 944
    assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['backoffice'] is True
943 945
    assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['channel'] == 'Mail'
......
960 962
    resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter=all', user=local_user))
961 963
    assert len(resp.json) == 30
962 964

  
965
def test_api_anonymized_formdata(pub, local_user):
966
    Role.wipe()
967
    role = Role(name='test')
968
    role.store()
969

  
970
    FormDef.wipe()
971
    formdef = FormDef()
972
    formdef.name = 'test'
973
    formdef.workflow_roles = {'_receiver': role.id}
974
    formdef.fields = [
975
        fields.StringField(id='0', label='foobar', varname='foobar'),
976
        fields.ItemField(id='1', label='foobar3', varname='foobar3', type='item',
977
            items=['foo', 'bar', 'baz']),
978
        fields.FileField(id='2', label='foobar4', varname='file'),
979
        ]
980
    formdef.store()
981

  
982
    data_class = formdef.data_class()
983
    data_class.wipe()
984

  
985
    for i in range(30):
986
        formdata = data_class()
987
        date = time.strptime('2014-01-20', '%Y-%m-%d')
988
        upload = PicklableUpload('test.txt', 'text/plain', 'ascii')
989
        upload.receive(['base64me'])
990
        formdata.data = {'0': 'FOO BAR %d' % i, '2': upload}
991
        formdata.user_id = local_user.id
992
        if i%4 == 0:
993
            formdata.data['1'] = 'foo'
994
            formdata.data['1_display'] = 'foo'
995
        elif i%4 == 1:
996
            formdata.data['1'] = 'bar'
997
            formdata.data['1_display'] = 'bar'
998
        else:
999
            formdata.data['1'] = 'baz'
1000
            formdata.data['1_display'] = 'baz'
1001

  
1002
        formdata.just_created()
1003
        if i%3 == 0:
1004
            formdata.jump_status('new')
1005
        else:
1006
            formdata.jump_status('finished')
1007
        formdata.store()
1008

  
1009
    # check access is granted even if the user has not the appropriate role
1010
    resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on', user=local_user))
1011
    assert len(resp.json) == 30
1012
    assert 'receipt_time' in resp.json[0]
1013
    assert 'fields' in resp.json[0]
1014
    assert 'user' not in resp.json[0]
1015
    assert 'file' not in resp.json[0]['fields'] # no file export in full lists
1016
    assert 'foobar3' in resp.json[0]['fields']
1017
    assert 'foobar' not in resp.json[0]['fields']
1018

  
1019
    # check access is granted event if there is no user
1020
    resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on'))
1021
    assert len(resp.json) == 30
1022
    assert 'receipt_time' in resp.json[0]
1023
    assert 'fields' in resp.json[0]
1024
    assert 'user' not in resp.json[0]
1025
    assert 'file' not in resp.json[0]['fields'] # no file export in full lists
1026
    assert 'foobar3' in resp.json[0]['fields']
1027
    assert 'foobar' not in resp.json[0]['fields']
1028
    # check anonymise is enforced on detail view
1029
    resp = get_app(pub).get(sign_uri('/api/forms/%s/?anonymise&full=on' % resp.json[0]['id']))
1030
    assert 'receipt_time' in resp.json
1031
    assert 'fields' in resp.json
1032
    assert 'user' not in resp.json
1033
    assert 'file' not in resp.json['fields'] # no file export in detail
1034
    assert 'foobar3' in resp.json['fields']
1035
    assert 'foobar' not in resp.json['fields']
1036

  
963 1037
def test_roles(pub, local_user):
964 1038
    Role.wipe()
965 1039
    role = Role(name='Hello World')
wcs/api.py
52 52

  
53 53

  
54 54
class ApiFormPage(BackofficeFormPage):
55
    _q_exports = [('list', 'json')] # same as backoffice but restricted to json export
55
    _q_exports = [('list', 'json')] # restrict to API endpoints
56 56

  
57 57
    def __init__(self, component):
58 58
        try:
......
63 63
        # otherwise be accessible if the user is the submitter.
64 64
        self.check_access()
65 65

  
66

  
66 67
    def check_access(self):
67
        api_user = get_user_from_api_query_string()
68
        if not api_user:
69
            if get_request().user and get_request().user.is_admin:
70
                return # grant access to admins, to ease debug
71
            raise AccessForbiddenError('user not authenticated')
72
        if not self.formdef.is_of_concern_for_user(api_user):
73
            raise AccessForbiddenError('unsufficient roles')
68
        if 'anonymise' in get_request().form:
69
            if not is_url_signed() or (get_request().user and get_request().user.is_admin):
70
                raise AccessForbiddenError('user not authenticated')
71
        else:
72
            api_user = get_user_from_api_query_string()
73
            if not api_user:
74
                if get_request().user and get_request().user.is_admin:
75
                    return # grant access to admins, to ease debug
76
                raise AccessForbiddenError('user not authenticated')
77
            if not self.formdef.is_of_concern_for_user(api_user):
78
                raise AccessForbiddenError('unsufficient roles')
74 79

  
75 80
    def _q_lookup(self, component):
76 81
        try:
......
82 87

  
83 88
class ApiFormsDirectory(Directory):
84 89
    def _q_lookup(self, component):
85
        api_user = get_user_from_api_query_string()
86
        if not api_user:
90
        if not is_url_signed():
87 91
            # grant access to admins, to ease debug
88 92
            if not (get_request().user and get_request().user.is_admin):
89 93
                raise AccessForbiddenError('user not authenticated')
90
-