From bb30acf55bf4f07b553a59e865a597982bae693c Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 9 Jul 2024 15:56:18 -0400 Subject: [PATCH] Refs #254. Allowed participants to query assets for a session they created. --- .../DBManagerTeraParticipantAccess.py | 4 +- .../API/participant/ParticipantQueryAssets.py | 25 ++++-- .../python/opentera/db/models/TeraSession.py | 2 + .../test_ParticipantQueryAssets.py | 89 ++++++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraParticipantAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraParticipantAccess.py index 2cd666cec..703df4686 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraParticipantAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraParticipantAccess.py @@ -32,7 +32,7 @@ def query_device(self, filters: dict): filter_by(id_participant=self.participant.id_participant).all() return result - def get_accessible_assets(self, id_asset: int = None, uuid_asset: str = None): + def get_accessible_assets(self, id_asset: int = None, uuid_asset: str = None, session_id: int = None): from opentera.db.models.TeraAsset import TeraAsset # A participant can only have access to assets that are directly assigned to them (where id_participant is set @@ -42,6 +42,8 @@ def get_accessible_assets(self, id_asset: int = None, uuid_asset: str = None): query = query.filter(TeraAsset.id_asset == id_asset) elif uuid_asset: query = query.filter(TeraAsset.asset_uuid == uuid_asset) + elif session_id: + query = query.filter(TeraAsset.id_session == session_id) return query.all() diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py index d355987b8..16dc80c15 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py @@ -1,5 +1,6 @@ from flask import session, request from flask_restx import Resource, inputs +from flask_babel import gettext from modules.LoginModule.LoginModule import participant_multi_auth, current_participant from modules.DatabaseModule.DBManager import DBManager from modules.FlaskModule.FlaskModule import device_api_ns as api @@ -12,6 +13,7 @@ get_parser = api.parser() get_parser.add_argument('asset_uuid', type=str, help='Asset UUID to query', default=None) get_parser.add_argument('id_asset', type=int, help='Asset ID to query', default=None) +get_parser.add_argument('id_session', type=int, help='Session ID to query assets for', default=None) get_parser.add_argument('with_urls', type=inputs.boolean, help='Also include assets infos and download-upload url') get_parser.add_argument('with_only_token', type=inputs.boolean, help='Only includes the access token. ' 'Will ignore with_urls if specified.') @@ -29,11 +31,22 @@ def __init__(self, _api, *args, **kwargs): 403: 'Participant doesn\'t have access to the specified asset'}, params={'token': 'Secret token'}) @api.expect(get_parser) - @participant_multi_auth.login_required(role='full') + @participant_multi_auth.login_required(role='limited') def get(self): args = get_parser.parse_args() participant_access = DBManager.participantAccess(current_participant) - assets = participant_access.get_accessible_assets(id_asset=args['id_asset'], uuid_asset=args['asset_uuid']) + + if not current_participant.fullAccess: + # Not full access = can only query by id_asset or id_session + if not args['id_session'] and not args['id_asset'] and not args['asset_uuid']: + return gettext('Forbidden'), 403 + + if args['id_session']: + if args['id_session'] not in participant_access.get_accessible_sessions_ids(): + return gettext('No access to session'), 403 + + assets = participant_access.get_accessible_assets(id_asset=args['id_asset'], uuid_asset=args['asset_uuid'], + session_id=args['id_session']) # Create response servername = self.module.config.server_config['hostname'] @@ -45,19 +58,19 @@ def get(self): if 'X_EXTERNALPORT' in request.headers: port = request.headers['X_EXTERNALPORT'] services_infos = [] - if (args['with_urls'] or args['with_only_token']) and assets: + if (args['with_urls'] or args['with_only_token']) and assets and current_participant.fullAccess: services_infos = {service.service_uuid: service.service_clientendpoint for service in participant_access.get_accessible_services()} assets_json = [] for asset in assets: - if args['with_only_token']: + if args['with_only_token'] and current_participant.fullAccess: asset_json = {'asset_uuid': asset.asset_uuid} else: asset_json = asset.to_json() # Access token - if args['with_urls'] or args['with_only_token']: + if (args['with_urls'] or args['with_only_token']) and current_participant.fullAccess: # Access token token_key = self.module.redisGet(RedisVars.RedisVar_ServiceTokenAPIKey) access_token = TeraAsset.get_access_token(asset_uuids=asset.asset_uuid, @@ -65,7 +78,7 @@ def get(self): requester_uuid=current_participant.participant_uuid, expiration=1800) asset_json['access_token'] = access_token - if args['with_urls']: + if args['with_urls'] and current_participant.fullAccess: # We have previously verified that the service is available to the user if asset.asset_service_uuid in services_infos: asset_json['asset_infos_url'] = 'https://' + servername + ':' + str(port) \ diff --git a/teraserver/python/opentera/db/models/TeraSession.py b/teraserver/python/opentera/db/models/TeraSession.py index 9d002f203..96316cd75 100644 --- a/teraserver/python/opentera/db/models/TeraSession.py +++ b/teraserver/python/opentera/db/models/TeraSession.py @@ -170,6 +170,8 @@ def create_defaults(test=False): base_session.session_users = [base_session.session_creator_user, session_user2, session_user3] if i == 3: base_session.session_devices = [session_device] + if i == 1: + base_session.id_creator_participant = session_part.id_participant base_session.session_uuid = str(uuid.uuid4()) TeraSession.db().session.add(base_session) diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py index 44e973f35..cdc4b7627 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py @@ -1,5 +1,8 @@ +from typing import List from BaseParticipantAPITest import BaseParticipantAPITest from opentera.db.models.TeraAsset import TeraAsset +from opentera.db.models.TeraParticipant import TeraParticipant +from opentera.db.models.TeraSession import TeraSession class ParticipantQueryAssetsTest(BaseParticipantAPITest): @@ -35,11 +38,19 @@ def test_query_invalid_token_auth(self): def test_static_token(self): with self._flask_app.app_context(): - params = {'id_asset': 1, 'with_urls': True} + params = {'with_urls': True} response = self._get_with_participant_token_auth(self.test_client, token=self.participant_static_token, params=params) self.assertEqual(403, response.status_code) + def test_static_token_forbidden_id(self): + with self._flask_app.app_context(): + params = {'id_asset': 10, 'with_urls': True} + response = self._get_with_participant_token_auth(self.test_client, + token=self.participant_static_token, params=params) + self.assertEqual(200, response.status_code) + self.assertEqual(0, len(response.json)) + def test_query_assets_get_id(self): with self._flask_app.app_context(): params = {'id_asset': 1, 'with_urls': True} @@ -106,6 +117,78 @@ def test_query_assets_all_token_only(self): self.assertTrue(data_item.__contains__("asset_uuid")) self.assertTrue(data_item.__contains__("access_token")) + def test_get_endpoint_with_token_auth_with_forbidden_id_session(self): + with self._flask_app.app_context(): + participants: List[TeraParticipant] = TeraParticipant.query.all() + + for participant in participants: + for session in TeraSession.query.all(): + params = { + 'id_session': session.id_session, + 'with_urls': True + } + + if participant.participant_token: + if session.id_creator_participant != participant.id_participant: + response = self._get_with_participant_token_auth(self.test_client, + token=participant.participant_token, + params=params) + self.assertEqual(403, response.status_code) + + def test_get_endpoint_with_token_auth_with_session_id(self): + with self._flask_app.app_context(): + participants: List[TeraParticipant] = TeraParticipant.query.all() + + for participant in participants: + for session in TeraSession.query.all(): + params = { + 'id_session': session.id_session, + 'with_urls': True + } + + if participant.participant_token: + if session.id_creator_participant == participant.id_participant: + response = self._get_with_participant_token_auth(self.test_client, + token=participant.participant_token, + params=params) + self.assertEqual(200, response.status_code) + + assets_ids = [asset.id_asset for asset in + TeraAsset.get_assets_for_session(session.id_session) + if asset.id_participant == participant.id_participant] + for asset_json in response.json: + self.assertTrue(asset_json['id_asset'] in assets_ids) + self._checkJson(asset_json, minimal=True) # Participant with token = never return url + assets_ids.remove(asset_json['id_asset']) + self.assertFalse(assets_ids) + + def test_get_endpoint_with_login_with_session_id(self): + with self._flask_app.app_context(): + participants: List[TeraParticipant] = TeraParticipant.query.all() + + for participant in participants: + for session in TeraSession.query.all(): + params = { + 'id_session': session.id_session, + 'with_urls': True + } + + if participant.participant_login_enabled: + if session.id_creator_participant == participant.id_participant: + response = self._get_with_participant_http_auth(self.test_client, + username=participant.participant_username, + password='opentera', params=params) + self.assertEqual(200, response.status_code) + + assets_ids = [asset.id_asset for asset in + TeraAsset.get_assets_for_session(session.id_session) + if asset.id_participant == participant.id_participant] + for asset_json in response.json: + self.assertTrue(asset_json['id_asset'] in assets_ids) + self._checkJson(asset_json, minimal=False) # Full infos + assets_ids.remove(asset_json['id_asset']) + self.assertFalse(assets_ids) + def _checkJson(self, json_data, minimal=False): self.assertGreater(len(json_data), 0) self.assertTrue(json_data.__contains__('id_asset')) @@ -123,3 +206,7 @@ def _checkJson(self, json_data, minimal=False): self.assertTrue(json_data.__contains__('asset_infos_url')) self.assertTrue(json_data.__contains__('asset_url')) self.assertTrue(json_data.__contains__('access_token')) + else: + self.assertFalse(json_data.__contains__('asset_infos_url')) + self.assertFalse(json_data.__contains__('asset_url')) + self.assertFalse(json_data.__contains__('access_token'))