Projet

Général

Profil

0003-arcgis-add-token-system-63825.patch

Thomas Noël, 23 mai 2022 15:00

Télécharger (11 ko)

Voir les différences:

Subject: [PATCH 3/3] arcgis: add token system (#63825)

 .../apps/arcgis/migrations/0007_token.py      |  27 ++++
 passerelle/apps/arcgis/models.py              |  65 ++++++++--
 tests/test_arcgis.py                          | 119 ++++++++++++++++++
 3 files changed, 200 insertions(+), 11 deletions(-)
 create mode 100644 passerelle/apps/arcgis/migrations/0007_token.py
passerelle/apps/arcgis/migrations/0007_token.py
1
# Generated by Django 2.2.26 on 2022-05-12 15:02
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('arcgis', '0006_auto_20200401_1025'),
10
    ]
11

  
12
    operations = [
13
        migrations.AddField(
14
            model_name='arcgis',
15
            name='token_password',
16
            field=models.CharField(
17
                blank=True, max_length=128, verbose_name='Password of the user who wants to get a token'
18
            ),
19
        ),
20
        migrations.AddField(
21
            model_name='arcgis',
22
            name='token_username',
23
            field=models.CharField(
24
                blank=True, max_length=128, verbose_name='User name of the user who wants to get a token'
25
            ),
26
        ),
27
    ]
passerelle/apps/arcgis/models.py
42 42

  
43 43
    base_url = models.URLField(_('Webservice Base URL'))
44 44

  
45
    token_username = models.CharField(
46
        max_length=128, verbose_name=_('User name of the user who wants to get a token'), blank=True
47
    )
48
    token_password = models.CharField(
49
        max_length=128, verbose_name=_('Password of the user who wants to get a token'), blank=True
50
    )
51

  
45 52
    class Meta:
46 53
        verbose_name = _('ArcGIS REST API')
47 54

  
55
    def request(self, url, params=None, data=None, query_type='query'):
56
        if data:
57
            response = self.requests.post(url, params=params, data=data)
58
        else:
59
            response = self.requests.get(url, params=params, data=data)
60
        if response.status_code // 100 != 2:
61
            raise ArcGISError('ArcGIS/%s returned status code %s' % (query_type, response.status_code))
62
        try:
63
            response = response.json()
64
        except ValueError:
65
            raise ArcGISError('ArcGIS/%s returned invalid JSON content: %r' % (query_type, response.content))
66
        if not isinstance(response, dict):
67
            raise ArcGISError('ArcGIS/%s not returned a dict: %r' % (query_type, response))
68
        if 'error' in response:
69
            err_desc = response['error'].get('message') or ('unknown ArcGIS/%s error' % query_type)
70
            raise ArcGISError(err_desc, data=response)
71
        return response
72

  
73
    def generate_token(self):
74
        if not self.token_username and not self.token_password:
75
            return None
76
        url = urlparse.urljoin(self.base_url, 'info')
77
        info = self.request(url, params={'f': 'json'}, query_type='token')
78
        token_url = info.get('authInfo', {}).get('tokenServicesUrl')
79
        if not token_url:
80
            raise ArcGISError('ArcGIS/token responded no authInfo/tokenServicesUrl in info: %r' % info)
81
        response = self.request(
82
            token_url,
83
            data={
84
                'username': self.token_username,
85
                'password': self.token_password,
86
                'client': 'referer',
87
                'referer': urlparse.urljoin(self.base_url, 'services'),
88
                'f': 'json',
89
            },
90
            query_type='token',
91
        )
92
        if 'token' not in response:
93
            raise ArcGISError('ArcGIS/token returned no token: %r' % response)
94
        return response['token']
95

  
48 96
    def build_common_params(self, lat, lon, latmin, lonmin, latmax, lonmax, **kwargs):
49 97
        # build common query params, see:
50 98
        # https://developers.arcgis.com/rest/services-reference/query-map-service-layer-.htm
......
74 122
        params.update(kwargs)
75 123
        if 'distance' in params and 'units' not in params:
76 124
            params['units'] = 'esriSRUnit_Meter'
125
        # add a token if applicable
126
        if 'token' not in params:
127
            token = self.generate_token()
128
            if token is not None:
129
                params['token'] = token
77 130
        return params
78 131

  
79 132
    def get_query_response(self, uri, params, id_template, text_template, full, text_fieldname=None):
80 133
        url = urlparse.urljoin(self.base_url, uri)
81
        response = self.requests.get(url, params=params)
82

  
83
        if response.status_code // 100 != 2:
84
            raise ArcGISError('ArcGIS returned status code %s' % response.status_code)
85
        try:
86
            infos = response.json()
87
        except (ValueError,):
88
            raise ArcGISError('ArcGIS returned invalid JSON content: %r' % response.content)
89
        if 'error' in infos:
90
            err_desc = infos['error'].get('message') or 'unknown ArcGIS error'
91
            raise ArcGISError(err_desc, data=infos)
134
        infos = self.request(url, params=params)
92 135

  
93 136
        features = infos.pop('features', [])
94 137
        id_fieldname = infos.get('objectIdFieldName') or 'OBJECTID'
tests/test_arcgis.py
141 141
    }
142 142
}'''
143 143

  
144
INFO = '''{
145
    "authInfo": {
146
        "isTokenBasedSecurity": true,
147
        "tokenServicesUrl": "https://arcgis/portal/sharing/rest/generateToken"
148
    },
149
    "currentVersion": 10.81,
150
    "fullVersion": "10.8.1",
151
    "owningSystemUrl": "https://arcgis/portal",
152
    "secureSoapUrl": null,
153
    "soapUrl": "https://arcgis/arcgis/services"
154
}'''
155

  
156
TOKEN = '''{
157
    "expires": 1649761676673,
158
    "ssl": true,
159
    "token": "tok42"
160
}'''
161

  
144 162

  
145 163
@pytest.fixture
146 164
def arcgis():
......
534 552
        assert resp.json['err_desc'] == '<lonmin> <latmin> <lonmax> and <latmax> must be floats'
535 553

  
536 554

  
555
def test_arcgis_with_token_query(app, arcgis):
556
    endpoint = tests.utils.generic_endpoint_url('arcgis', 'featureservice-query', slug=arcgis.slug)
557
    assert endpoint == '/arcgis/test/featureservice-query'
558
    # open access
559
    api = ApiUser.objects.create(username='all', keytype='', key='')
560
    obj_type = ContentType.objects.get_for_model(arcgis)
561
    AccessRight.objects.create(
562
        codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=arcgis.pk
563
    )
564

  
565
    arcgis.token_username = 'tokenuser'
566
    arcgis.token_password = 'tokenpass'
567
    arcgis.save()
568
    params = {'folder': 'fold', 'service': 'serv', 'layer': '42'}
569

  
570
    with mock.patch('passerelle.utils.Request.get') as requests_get:
571
        with mock.patch('passerelle.utils.Request.post') as requests_post:
572
            requests_get.side_effect = [
573
                tests.utils.FakedResponse(content=INFO, status_code=200),
574
                tests.utils.FakedResponse(content=FEATURES, status_code=200),
575
            ]
576
            requests_post.side_effect = [
577
                tests.utils.FakedResponse(content=TOKEN, status_code=200),
578
            ]
579
            resp = app.get(endpoint, params=params, status=200)
580
            assert requests_get.call_count == 2  # info + featureservice-query
581
            assert requests_post.call_count == 1  # token
582
            assert (
583
                requests_get.call_args[0][0]
584
                == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
585
            )
586
            args = requests_get.call_args[1]['params']
587
            assert args['f'] == 'json'
588
            assert args['outFields'] == '*'
589
            assert args['token'] == 'tok42'  # token added in query
590
            assert 'data' in resp.json
591
            assert resp.json['err'] == 0
592
            assert len(resp.json['data']) == 2
593
            assert resp.json['data'][0]['id'] == '11'
594
            assert resp.json['data'][0]['text'] == '11'
595

  
596
            # bad info/token responses
597

  
598
            requests_get.reset_mock()
599
            requests_post.reset_mock()
600
            requests_get.side_effect = [
601
                tests.utils.FakedResponse(content='{}', status_code=200),
602
            ]
603
            resp = app.get(endpoint, params=params, status=200)
604
            assert requests_get.call_count == 1
605
            assert requests_post.call_count == 0
606
            assert resp.json['err'] == 1
607
            assert resp.json['err_desc'].startswith(
608
                'ArcGIS/token responded no authInfo/tokenServicesUrl in info'
609
            )
610

  
611
            requests_get.reset_mock()
612
            requests_get.side_effect = [
613
                tests.utils.FakedResponse(content='CRASH', status_code=500),
614
            ]
615
            resp = app.get(endpoint, params=params, status=200)
616
            assert requests_get.call_count == 1
617
            assert requests_post.call_count == 0
618
            assert resp.json['err'] == 1
619
            assert resp.json['err_desc'] == 'ArcGIS/token returned status code 500'
620

  
621
            requests_get.reset_mock()
622
            requests_get.side_effect = [
623
                tests.utils.FakedResponse(content='not json', status_code=200),
624
            ]
625
            resp = app.get(endpoint, params=params, status=200)
626
            assert requests_get.call_count == 1
627
            assert requests_post.call_count == 0
628
            assert resp.json['err'] == 1
629
            assert resp.json['err_desc'].startswith('ArcGIS/token returned invalid JSON content:')
630

  
631
            requests_get.reset_mock()
632
            requests_get.side_effect = [
633
                tests.utils.FakedResponse(content='"not a dict"', status_code=200),
634
            ]
635
            resp = app.get(endpoint, params=params, status=200)
636
            assert requests_get.call_count == 1
637
            assert requests_post.call_count == 0
638
            assert resp.json['err'] == 1
639
            assert resp.json['err_desc'].startswith('ArcGIS/token not returned a dict:')
640

  
641
            requests_get.reset_mock()
642
            requests_post.reset_mock()
643
            requests_get.side_effect = [
644
                tests.utils.FakedResponse(content=INFO, status_code=200),
645
            ]
646
            requests_post.side_effect = [
647
                tests.utils.FakedResponse(content="{}", status_code=200),  # no token
648
            ]
649
            resp = app.get(endpoint, params=params, status=200)
650
            assert requests_get.call_count == 1
651
            assert requests_post.call_count == 1
652
            assert resp.json['err'] == 1
653
            assert resp.json['err_desc'].startswith('ArcGIS/token returned no token:')
654

  
655

  
537 656
@pytest.mark.parametrize(
538 657
    'format_string,fail',
539 658
    [
540
-