api: allow mixing anonymous restriction and basic authentication (#53883)

This commit is contained in:
Frédéric Péters 2021-05-10 13:47:51 +02:00
parent 3a641631b1
commit e8a234da11
5 changed files with 64 additions and 34 deletions

View File

@ -58,6 +58,8 @@ coucou = 1234
''' '''
) )
pub.user_class.wipe()
return pub return pub
@ -67,7 +69,6 @@ def teardown_module(module):
@pytest.fixture @pytest.fixture
def local_user(): def local_user():
get_publisher().user_class.wipe()
user = get_publisher().user_class() user = get_publisher().user_class()
user.name = 'Jean Darmette' user.name = 'Jean Darmette'
user.email = 'jean.darmette@triffouilis.fr' user.email = 'jean.darmette@triffouilis.fr'
@ -78,7 +79,6 @@ def local_user():
@pytest.fixture @pytest.fixture
def admin_user(): def admin_user():
get_publisher().user_class.wipe()
user = get_publisher().user_class() user = get_publisher().user_class()
user.name = 'John Doe Admin' user.name = 'John Doe Admin'
user.email = 'john.doe@example.com' user.email = 'john.doe@example.com'
@ -788,7 +788,8 @@ def test_api_anonymized_formdata(pub, local_user, admin_user):
assert 'name' in resp.json['evolution'][1]['who'] assert 'name' in resp.json['evolution'][1]['who']
def test_api_access_restrict_to_anonymised_data(pub, local_user): @pytest.mark.parametrize('http_basic_auth', [False, True])
def test_api_access_restrict_to_anonymised_data(pub, local_user, http_basic_auth):
pub.role_class.wipe() pub.role_class.wipe()
role = pub.role_class(name='test') role = pub.role_class(name='test')
role.store() role.store()
@ -823,36 +824,56 @@ def test_api_access_restrict_to_anonymised_data(pub, local_user):
access.access_key = '12345' access.access_key = '12345'
access.store() access.store()
resp = get_app(pub).get( app = get_app(pub)
sign_uri(
'/api/forms/test/list?full=on', if http_basic_auth:
user=local_user, # there's not "defaults to admin" permissions in case of basic authentication.
orig=access.access_identifier, access.roles = [role]
key=access.access_key, access.store()
)
) def get_url(url, **kwargs):
app.set_authorization(('Basic', ('test', '12345')))
return app.get(url, **kwargs)
else:
def get_url(url, **kwargs):
return app.get(
sign_uri(url, user=local_user, orig=access.access_identifier, key=access.access_key), **kwargs
)
resp = get_url('/api/forms/test/list?full=on')
assert len(resp.json) == 10 assert len(resp.json) == 10
assert resp.json[0]['fields']['foobar'] == 'FOO BAR1' assert resp.json[0]['fields']['foobar'] == 'FOO BAR1'
assert resp.json[0]['fields']['foobar2'] == 'FOO BAR 2' assert resp.json[0]['fields']['foobar2'] == 'FOO BAR 2'
assert resp.json[0].get('user') assert resp.json[0].get('user')
# get a single formdata
resp = get_url('/api/forms/test/%s/' % formdata.id)
assert 'user' in resp.json
# restrict API access to anonymised data # restrict API access to anonymised data
access.restrict_to_anonymised_data = True access.restrict_to_anonymised_data = True
access.store() access.store()
resp = get_app(pub).get( resp = get_url('/api/forms/test/list?full=on')
sign_uri(
'/api/forms/test/list?full=on',
user=local_user,
orig=access.access_identifier,
key=access.access_key,
)
)
assert len(resp.json) == 10 assert len(resp.json) == 10
assert 'foobar' not in resp.json[0]['fields'] assert 'foobar' not in resp.json[0]['fields']
assert resp.json[0]['fields']['foobar2'] == 'FOO BAR 2' assert resp.json[0]['fields']['foobar2'] == 'FOO BAR 2'
assert not resp.json[0].get('user') assert not resp.json[0].get('user')
# get a single formdata
resp = get_url('/api/forms/test/%s/' % formdata.id)
assert 'user' not in resp.json
if http_basic_auth:
# for basic HTTP authentication, check there's no access if roles are not given.
access.roles = []
access.store()
get_url('/api/forms/test/list?full=on', status=403)
get_url('/api/forms/test/%s/' % formdata.id, status=403)
def test_api_geojson_formdata(pub, local_user): def test_api_geojson_formdata(pub, local_user):
pub.role_class.wipe() pub.role_class.wipe()

View File

@ -189,17 +189,19 @@ class ApiFormPageMixin:
raise TraversalError() raise TraversalError()
def check_access(self, api_name=None): def check_access(self, api_name=None):
if get_request().has_anonymised_data_api_restriction(): if get_request().user and get_request().user.is_admin:
if not is_url_signed() or (get_request().user and get_request().user.is_admin): return # grant access to admins, to ease debug
raise AccessForbiddenError('user not authenticated')
else: if get_request().has_anonymised_data_api_restriction() and is_url_signed():
if get_request().user and get_request().user.is_admin: # when requesting anonymous data, a signature is enough
return # grant access to admins, to ease debug return
api_user = get_user_from_api_query_string(api_name=api_name)
if not api_user: api_user = get_user_from_api_query_string(api_name=api_name)
raise AccessForbiddenError('user not authenticated')
if not self.formdef.is_of_concern_for_user(api_user): if not api_user:
raise AccessForbiddenError('unsufficient roles') 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): def _q_lookup(self, component):
if component == 'ics': if component == 'ics':

View File

@ -89,6 +89,9 @@ class ApiAccess(XmlStorableObject):
is_api_user = True is_api_user = True
anonymous = False anonymous = False
def __init__(self, api_access):
self.api_access = api_access
def can_go_in_admin(self): def can_go_in_admin(self):
return False return False
@ -98,7 +101,7 @@ class ApiAccess(XmlStorableObject):
def get_roles(self): def get_roles(self):
return self.roles return self.roles
user = RestrictedApiUser() user = RestrictedApiUser(self)
user.roles = [x.id for x in self.get_roles()] user.roles = [x.id for x in self.get_roles()]
return user return user

View File

@ -158,13 +158,12 @@ class FormStatusPage(Directory, FormTemplateMixin):
session = get_session() session = get_session()
mine = False mine = False
if api_call: if api_call:
if get_request().has_anonymised_data_api_restriction(): user = get_user_from_api_query_string() or get_request().user
if get_request().has_anonymised_data_api_restriction() and (not user or not user.is_api_user):
if is_url_signed() or (get_request().user and get_request().user.is_admin): if is_url_signed() or (get_request().user and get_request().user.is_admin):
return None return None
else: else:
raise errors.AccessUnauthorizedError() raise errors.AccessUnauthorizedError()
else:
user = get_user_from_api_query_string() or get_request().user
else: else:
user = get_request().user user = get_request().user
if user and not user.anonymous: if user and not user.anonymous:

View File

@ -223,11 +223,16 @@ class HTTPRequest(quixote.http_request.HTTPRequest):
if 'anonymise' in self.form: if 'anonymise' in self.form:
return True return True
orig = self.form.get('orig') orig = self.form.get('orig')
if orig: if orig:
api_access = ApiAccess.get_by_identifier(orig) api_access = ApiAccess.get_by_identifier(orig)
if api_access: if api_access:
return api_access.restrict_to_anonymised_data return api_access.restrict_to_anonymised_data
if self.user and self.user.is_api_user:
return self.user.api_access.restrict_to_anonymised_data
return False return False
@property @property