0003-arcgis-add-token-system-63825.patch
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 |
- |