api: keep local cache of API clients from idp (#88697)

This commit is contained in:
Frédéric Péters 2024-03-27 07:55:42 +01:00
parent 6d4f720219
commit ee6d557f6e
4 changed files with 83 additions and 13 deletions

View File

@ -10,6 +10,7 @@ import zipfile
from contextlib import contextmanager
import pytest
import responses
from django.utils.encoding import force_bytes
from django.utils.timezone import localtime, make_aware
from quixote import get_publisher
@ -49,6 +50,12 @@ def pub(emails):
'''\
[api-secrets]
coucou = 1234
[variables]
idp_api_url = https://authentic.example.invalid/api/'
[wscall-secrets]
authentic.example.invalid = 4460cf12e156d841c116fbebd52d7ebe41282c63ac2605740068ba5fd89b7316
'''
)
@ -2985,9 +2992,12 @@ def test_api_distance_filter(pub, local_user):
get_app(pub).get(sign_uri('/api/forms/test/list?filter-distance=150000', user=local_user), status=400)
@pytest.mark.parametrize('user', ['query-email', 'api-access'])
@pytest.mark.parametrize('user', ['query-email', 'api-access', 'idp-api-client'])
@pytest.mark.parametrize('auth', ['signature', 'http-basic'])
@responses.activate
def test_api_ods_formdata(pub, local_user, user, auth):
ApiAccess.wipe()
pub.role_class.wipe()
role = pub.role_class(name='test')
role.store()
@ -3007,7 +3017,6 @@ def test_api_ods_formdata(pub, local_user, user, auth):
data_class.wipe()
if user == 'api-access':
ApiAccess.wipe()
access = ApiAccess()
access.name = 'test'
access.access_identifier = 'test'
@ -3025,6 +3034,29 @@ def test_api_ods_formdata(pub, local_user, user, auth):
def get_url(url, **kwargs):
return app.get(sign_uri(url, orig=access.access_identifier, key=access.access_key), **kwargs)
elif user == 'idp-api-client':
if auth == 'signature':
pytest.skip('signature authentication requires local user')
def get_url(url, **kwargs):
app.set_authorization(('Basic', ('test', '12345')))
return app.get(url, **kwargs)
responses.post(
'https://authentic.example.invalid/api/check-api-client/',
json={
'err': 0,
'data': {
'is_active': True,
'is_anonymous': False,
'is_authenticated': True,
'is_superuser': False,
'restrict_to_anonymised_data': False,
'roles': [],
},
},
)
else:
if auth == 'http-basic':
pytest.skip('http basic authentication requires ApiAccess')
@ -3053,6 +3085,21 @@ def test_api_ods_formdata(pub, local_user, user, auth):
if user == 'api-access':
access.roles = [role]
access.store()
elif user == 'idp-api-client':
responses.post(
'https://authentic.example.invalid/api/check-api-client/',
json={
'err': 0,
'data': {
'is_active': True,
'is_anonymous': False,
'is_authenticated': True,
'is_superuser': False,
'restrict_to_anonymised_data': False,
'roles': [role.id],
},
},
)
else:
local_user.roles = [role.id]
local_user.store()
@ -3081,6 +3128,14 @@ def test_api_ods_formdata(pub, local_user, user, auth):
formdef.store()
get_url('/api/forms/test/ods', status=200)
if user == 'idp-api-client':
# check a single api access object has been created
assert ApiAccess.count() == 1
api_access = ApiAccess.select()[0]
assert api_access.idp_api_client
assert api_access.access_identifier == '_idp_test'
assert api_access.access_key is None
def test_api_global_geojson(pub, local_user):
pub.role_class.wipe()

View File

@ -201,7 +201,7 @@ class ApiAccessDirectory(Directory):
templates=['wcs/backoffice/api_accesses.html'],
context={
'view': self,
'api_accesses': ApiAccess.select(order_by='name'),
'api_accesses': [x for x in ApiAccess.select(order_by='name') if not x.idp_api_client],
'api_manage_url': api_manage_url,
},
)

View File

@ -33,6 +33,7 @@ class ApiAccess(XmlStorableObject):
access_key = None
description = None
restrict_to_anonymised_data = False
idp_api_client = False
_roles = None
_role_ids = Ellipsis
@ -44,6 +45,7 @@ class ApiAccess(XmlStorableObject):
('access_key', 'str'),
('restrict_to_anonymised_data', 'bool'),
('roles', 'roles'),
('idp_api_client', 'bool'),
]
@classmethod
@ -98,7 +100,7 @@ class ApiAccess(XmlStorableObject):
@classmethod
def get_with_credentials(cls, username, password):
api_access = cls.get_by_identifier(username)
if not api_access or api_access.access_key != password:
if not api_access or api_access.access_key != password or api_access.idp_api_client:
api_access = cls.get_from_idp(username, password)
if not api_access:
raise KeyError
@ -143,11 +145,18 @@ class ApiAccess(XmlStorableObject):
if data.get('err', 1) != 0:
return None
api_access = cls.volatile()
# cache api client locally, it is necessary for serialization for afterjobs
# in uwsgi spooler.
access_identifier = f'_idp_{username}'
api_access = cls.get_by_identifier(access_identifier) or cls()
api_access.idp_api_client = True
api_access.access_identifier = access_identifier
role_class = get_publisher().role_class
try:
api_access.restrict_to_anonymised_data = data['data']['restrict_to_anonymised_data']
api_access._role_ids = data['data']['roles']
api_access.roles = [role_class.get(x, ignore_errors=True) for x in data['data']['roles']]
api_access.roles = [x for x in api_access.roles if x is not None]
except KeyError:
return None
api_access.store()
return api_access

View File

@ -3,10 +3,12 @@
{% block body %}
<div id="appbar">
<h2>{% trans "API access" %} - {{ api_access.name }}</h2>
<span class="actions">
<a href="delete" rel="popup">{% trans "Delete" %}</a>
<a href="edit">{% trans "Edit" %}</a>
</span>
{% if not api_access.idp_api_client %}
<span class="actions">
<a href="delete" rel="popup">{% trans "Delete" %}</a>
<a href="edit">{% trans "Edit" %}</a>
</span>
{% endif %}
</div>
{% if api_access.description %}
@ -16,8 +18,12 @@
<div class="bo-block">
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "Access identifier:" %} {{ api_access.access_identifier }}</li>
<li>{% trans "Access key:" %} {{ api_access.access_key }}</li>
{% if not api_access.idp_api_client %}
<li>{% trans "Access identifier:" %} {{ api_access.access_identifier }}</li>
<li>{% trans "Access key:" %} {{ api_access.access_key }}</li>
{% else %}
<li>{% trans "API client from identity provider, identifier:" %} {{ api_access.access_identifier|removeprefix:"_idp_" }}</li>
{% endif %}
{% if api_access.restrict_to_anonymised_data %}<li>{% trans "Restricted to anonymised data" %}</li>{% endif %}
{% if api_access.get_roles %}
<li>{% trans "Roles:" %}