From 8aabd7443a6ba7e53685c57a0d2594474022a470 Mon Sep 17 00:00:00 2001 From: David <9059044+Tansito@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:50:38 -0500 Subject: [PATCH 1/5] Authentication logic refactor --- .../charts/gateway/templates/deployment.yaml | 4 +- gateway/README.md | 5 +- gateway/api/authentication.py | 87 ++------- gateway/api/models_proxies.py | 114 ------------ gateway/api/repositories/users.py | 54 +++++- .../api/services/authentication/__init__.py | 0 .../authentication/quantum_platform.py | 175 ++++++++++++++++++ gateway/api/use_cases/__init__.py | 0 gateway/api/use_cases/authentication.py | 46 +++++ gateway/main/settings.py | 11 +- gateway/tests/api/test_authentication.py | 30 ++- gateway/tests/api/test_models_proxies.py | 142 -------------- 12 files changed, 302 insertions(+), 366 deletions(-) delete mode 100644 gateway/api/models_proxies.py create mode 100644 gateway/api/services/authentication/__init__.py create mode 100644 gateway/api/services/authentication/quantum_platform.py create mode 100644 gateway/api/use_cases/__init__.py create mode 100644 gateway/api/use_cases/authentication.py delete mode 100644 gateway/tests/api/test_models_proxies.py diff --git a/charts/qiskit-serverless/charts/gateway/templates/deployment.yaml b/charts/qiskit-serverless/charts/gateway/templates/deployment.yaml index e312cb95b..af16b0ddf 100644 --- a/charts/qiskit-serverless/charts/gateway/templates/deployment.yaml +++ b/charts/qiskit-serverless/charts/gateway/templates/deployment.yaml @@ -105,10 +105,8 @@ spec: value: {{ .Values.application.auth.mechanism | quote }} - name: SETTINGS_AUTH_MOCK_TOKEN value: {{ .Values.application.auth.token.mock | quote }} - - name: SETTINGS_TOKEN_AUTH_URL + - name: QUANTUM_PLATFORM_API_BASE_URL value: {{ .Values.application.auth.token.url | quote }} - - name: SETTINGS_TOKEN_AUTH_VERIFICATION_URL - value: {{ .Values.application.auth.token.verificationUrl | quote }} - name: SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD value: {{ .Values.application.auth.token.verificationField | quote }} - name: SETTINGS_AUTH_MOCKPROVIDER_REGISTRY diff --git a/gateway/README.md b/gateway/README.md index 76927bce1..c9b1c5a0d 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -19,11 +19,10 @@ docker build -t qiskit/qiskit-serverless/gateway: . | DJANGO_SUPERUSER_USERNAME | username for admin user that is created on launch of container | | DJANGO_SUPERUSER_PASSWORD | password for admin user that is created on launch of container | | DJANGO_SUPERUSER_EMAIL | email for admin user that is created on launch of container | -| SETTINGS_TOKEN_AUTH_URL | URL for custom token authentication | +| QUANTUM_PLATFORM_API_BASE_URL | URL for custom token authentication | | SETTINGS_TOKEN_AUTH_USER_FIELD | user field name for custom token authentication mechanism. Default `userId`. | | SETTINGS_TOKEN_AUTH_TOKEN_FIELD | user field name for custom token authentication mechanism. Default `apiToken`. | -| SETTINGS_AUTH_MECHANISM | authentication backend mechanism. Default `mock_token`. Options: `mock_token` and `custom_token`. If `custom_token` is selected then `SETTINGS_TOKEN_AUTH_URL` must be set. | -| SETTINGS_TOKEN_AUTH_VERIFICATION_URL | URL for custom token verificaiton | +| SETTINGS_AUTH_MECHANISM | authentication backend mechanism. Default `mock_token`. Options: `mock_token` and `custom_token`. If `custom_token` is selected then `QUANTUM_PLATFORM_API_BASE_URL` must be set. | | SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD | name of a field to use for token verification | | RAY_KUBERAY_NAMESPACE | namespace of kuberay resources. Should match kubernetes namespace | | RAY_NODE_IMAGE | Default node image that will be launched on ray cluster creation | diff --git a/gateway/api/authentication.py b/gateway/api/authentication.py index 2ed8a0f9d..18fed16c5 100644 --- a/gateway/api/authentication.py +++ b/gateway/api/authentication.py @@ -3,15 +3,13 @@ import logging from dataclasses import dataclass -import requests from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from rest_framework import authentication from api.models import VIEW_PROGRAM_PERMISSION, RUN_PROGRAM_PERMISSION, Provider -from api.models_proxies import QuantumUserProxy -from api.utils import safe_request +from api.use_cases.authentication import AuthenticationUseCase User = get_user_model() @@ -28,81 +26,24 @@ class CustomToken: class CustomTokenBackend(authentication.BaseAuthentication): """Custom token backend for authentication against 3rd party auth service.""" - def authenticate(self, request): # pylint: disable=too-many-branches - auth_url = settings.SETTINGS_TOKEN_AUTH_URL - verification_url = settings.SETTINGS_TOKEN_AUTH_VERIFICATION_URL - auth_header = request.META.get("HTTP_AUTHORIZATION") - + def authenticate(self, request): quantum_user = None - token = None - if auth_header is not None and auth_url is not None: - token = auth_header.split(" ")[-1] - - auth_data = safe_request( - request=lambda: requests.post( - auth_url, - json={settings.SETTINGS_TOKEN_AUTH_TOKEN_FIELD: token}, - timeout=60, - ) - ) - if auth_data is not None: - user_id = auth_data.get(settings.SETTINGS_TOKEN_AUTH_USER_FIELD) - - verification_data = safe_request( - request=lambda: requests.get( - verification_url, - headers={"Authorization": auth_data.get("id")}, - timeout=60, - ) - ) - - if verification_data is not None: - verifications = [] - for ( - verification_field - ) in settings.SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD.split(";"): - nested_field_value = verification_data - for nested_field in verification_field.split(","): - nested_field_value = nested_field_value.get(nested_field) - verifications.append(nested_field_value) - - verified = all(verifications) - - if user_id is not None and verified: - quantum_user, created = QuantumUserProxy.objects.get_or_create( - username=user_id - ) - if created: - logger.info("New user created") - quantum_user.update_groups(auth_data.get("id")) - - elif user_id is None: - logger.warning("Problems authenticating: No user id.") - - else: # not verified - logger.warning("Problems authenticating: User is not verified.") - - else: # verification_data is None - logger.warning( - "Problems authenticating: No verification data returned from request." - ) - - else: # auth_data is None - logger.warning( - "Problems authenticating: No authorization data returned from auth url." - ) - - elif auth_header is None: + authorization_token = None + auth_header = request.META.get("HTTP_AUTHORIZATION") + if auth_header is None: logger.warning( - "Problems authenticating: User did not provide authorization token." + "Problems authenticating: user did not provide authorization token." ) + return quantum_user, CustomToken(authorization_token.encode()) + authorization_token = auth_header.split(" ")[-1] - else: # auth_url is None - logger.warning( - "Problems authenticating: No auth url: something is broken in our settings." - ) + quantum_user = AuthenticationUseCase( + authorization_token=authorization_token + ).execute() + if quantum_user is None: + return quantum_user, CustomToken(authorization_token.encode()) - return quantum_user, CustomToken(token.encode()) if token else None + return quantum_user, CustomToken(authorization_token.encode()) class MockAuthBackend(authentication.BaseAuthentication): diff --git a/gateway/api/models_proxies.py b/gateway/api/models_proxies.py deleted file mode 100644 index f2beede7d..000000000 --- a/gateway/api/models_proxies.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Proxies for database models""" - -from typing import List -import logging -import requests - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, Permission - -from api.models import VIEW_PROGRAM_PERMISSION -from api.utils import safe_request, remove_duplicates_from_list - - -logger = logging.getLogger("gateway.authentication") - - -class QuantumUserProxy(get_user_model()): # pylint: disable=too-few-public-methods - """ - This proxy manages custom values for QuantumUser users like instances. - """ - - instances_url = f"{settings.IQP_QCON_API_BASE_URL}/network" - - class Meta: # pylint: disable=too-few-public-methods - """Proxy configuration""" - - proxy = True - - def _get_network(self, access_token: str): - """Obtain network configuration for a specific user: - Args: - access_token: IQP user token - - Returns: - network: { - name: str, - groups: { - str: { - name: - projects: { - str: { - name: str - } - } - } - } - } - """ - logger.info("Get Network information from [%s]", self.instances_url) - if self.instances_url is None: - return [] - - return safe_request( - request=lambda: requests.get( - self.instances_url, - headers={"Authorization": access_token}, - timeout=60, - ) - ) - - def _get_instances_from_network(self, network) -> List[str]: - """ - Returns an array of instances from network configuration. - Args: - network: Quantum User IQP Network configuration - - Returns: - List of instances, ex: - ["hub/group/project"] -> ["hub", "hub/project", "hub/project/group"] - """ - instances = [] - if network: # pylint: disable=too-many-nested-blocks - logger.info("Network exists, generate instances from network") - for hub in network: - instances.append(hub.get("name")) - groups = hub.get("groups") - if groups: - for group in groups.values(): - instances.append(f"{hub.get('name')}/{group.get('name')}") - projects = group.get("projects") - if projects: - for project in projects.values(): - instances.append( - f"{hub.get('name')}/{group.get('name')}/{project.get('name')}" - ) - return instances - - def update_groups(self, access_token) -> None: - """ - This method obtains the instances of a user from IQP Network User information - and update Django Groups with that information. - Args: - access_token: IQP user token - """ - network = self._get_network(access_token) - instances = self._get_instances_from_network(network) - - # Initially the list generated should not contain duplicates - # but this is just to sanitize the entry - logger.info("Remove duplicates from instances") - unique_instances = remove_duplicates_from_list(instances) - - logger.info("Clean user groups before update them") - self.groups.clear() - - logger.info("Update [%s] groups", len(unique_instances)) - view_program = Permission.objects.get(codename=VIEW_PROGRAM_PERMISSION) - for instance in unique_instances: - group, created = Group.objects.get_or_create(name=instance) - if created: - logger.info("New group created") - group.permissions.add(view_program) - group.user_set.add(self) diff --git a/gateway/api/repositories/users.py b/gateway/api/repositories/users.py index 5827e4478..7e3ebf175 100644 --- a/gateway/api/repositories/users.py +++ b/gateway/api/repositories/users.py @@ -2,16 +2,42 @@ Repository implementation for Groups model """ +import logging from typing import List -from django.contrib.auth.models import Group, Permission +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AbstractUser, Group, Permission from django.db.models import Q +from api.models import VIEW_PROGRAM_PERMISSION -class UserRepository: # pylint: disable=too-few-public-methods + +User = get_user_model() +logger = logging.getLogger("gateway.repositories.user") + + +class UserRepository: """ The main objective of this class is to manage the access to the model """ + def get_or_create_by_id(self, user_id: str) -> type[AbstractUser]: + """ + This method returns a user by its id. If the user does not + exist its created. + + Args: + user_id: id of the user + + Returns: + List[Group]: all the groups available to the user + """ + + user, created = User.objects.get_or_create(username=user_id) + if created: + logger.debug("New user created") + + return user + def get_groups_by_permissions(self, user, permission_name: str) -> List[Group]: """ Returns all the groups associated to a permission available in the user. @@ -29,3 +55,27 @@ def get_groups_by_permissions(self, user, permission_name: str) -> List[Group]: permission_criteria = Q(permissions=function_permission) return Group.objects.filter(user_criteria & permission_criteria) + + def restart_user_groups( + self, user: type[AbstractUser], unique_group_names: List[str] + ) -> None: + """ + This method will restart all the groups from a user given a specific list + with the new groups. + + Args: + user: Django user + unique_group_names List[str]: list with the names of the new groups + """ + + logger.debug("Clean user groups before update them") + user.groups.clear() + + logger.debug("Update [%s] groups", len(unique_group_names)) + view_program = Permission.objects.get(codename=VIEW_PROGRAM_PERMISSION) + for instance in unique_group_names: + group, created = Group.objects.get_or_create(name=instance) + if created: + logger.debug("New group created") + group.permissions.add(view_program) + group.user_set.add(user) diff --git a/gateway/api/services/authentication/__init__.py b/gateway/api/services/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/services/authentication/quantum_platform.py b/gateway/api/services/authentication/quantum_platform.py new file mode 100644 index 000000000..323cfb0c3 --- /dev/null +++ b/gateway/api/services/authentication/quantum_platform.py @@ -0,0 +1,175 @@ +"""This service will manage the access to the 3rd party end-points in Quantum platform.""" + + +import logging +from typing import List +from django.conf import settings +import requests + +from api.utils import remove_duplicates_from_list, safe_request + +logger = logging.getLogger("gateway.services.quantum_platform") + + +class QuantumPlatformService: + """ + This class will manage the different access to the different + end-points that we will make us of them in this service. + """ + + def __init__(self, authorization_token): + self.auth_url = ( + f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/api/users/loginWithToken" + ) + self.verification_url = f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/api/users/me" + self.instances_url = f"{settings.IQP_QCON_API_BASE_URL}/network" + self.authorization_token = authorization_token + self.access_token = None + + def _get_network(self, access_token: str): + """Obtain network configuration for a specific user: + Args: + access_token: IQP user token + + Returns: + network: { + name: str, + groups: { + str: { + name: + projects: { + str: { + name: str + } + } + } + } + } + """ + logger.debug("Get Network information from [%s]", self.instances_url) + if self.instances_url is None: + return [] + + return safe_request( + request=lambda: requests.get( + self.instances_url, + headers={"Authorization": access_token}, + timeout=60, + ) + ) + + def _get_instances_from_network(self, network) -> List[str]: + """ + Returns an array of instances from network configuration. + Args: + network: Quantum User IQP Network configuration + + Returns: + List of instances, ex: + ["hub/group/project"] -> ["hub", "hub/project", "hub/project/group"] + """ + instances = [] + if network: # pylint: disable=too-many-nested-blocks + logger.debug("Network exists, generate instances from network") + for hub in network: + instances.append(hub.get("name")) + groups = hub.get("groups") + if groups: + for group in groups.values(): + instances.append(f"{hub.get('name')}/{group.get('name')}") + projects = group.get("projects") + if projects: + for project in projects.values(): + instances.append( + f"{hub.get('name')}/{group.get('name')}/{project.get('name')}" + ) + return instances + + def authenticate(self) -> str | None: + """ + This method authenticates the user with the token provided in the + instantiation of the class and populates the access_token attribute. + + Ideally this method should be called first. + """ + if self.auth_url is None: + logger.warning( + "Problems authenticating: No auth url: something is broken in our settings." + ) + return None + + auth_data = safe_request( + request=lambda: requests.post( + self.auth_url, + json={"apiToken": self.authorization_token}, + timeout=60, + ) + ) + if auth_data is None: + logger.warning( + "Problems authenticating: No authorization data returned from auth url." + ) + return None + + user_id = auth_data.get("userId") + if user_id is None: + logger.warning("Problems authenticating: No user id.") + return None + + self.access_token = auth_data.get("id") + if self.access_token is None: + logger.warning("Problems authenticating: No access token.") + return None + + return user_id + + def verify_access(self) -> bool: + """ + This method validates that the user has access to Quantum Functions. + In this specific case the most important validation is the ibmQNetwork + field. + """ + verification_data = safe_request( + request=lambda: requests.get( + self.verification_url, + headers={"Authorization": self.access_token}, + timeout=60, + ) + ) + if verification_data is None: + logger.warning( + "Problems authenticating: No verification data returned from request." + ) + return False + + verifications = [] + verification_fields = settings.SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD.split(";") + for verification_field in verification_fields: + nested_field_value = verification_data + for nested_field in verification_field.split(","): + nested_field_value = nested_field_value.get(nested_field) + verifications.append(nested_field_value) + + verified = all(verifications) + if verified is False: + logger.warning("Problems authenticating: User is not verified.") + + return verified + + def get_groups(self) -> List[str]: + """ + Returns an array of instances from network configuration. + Args: + network: Quantum User IQP Network configuration + + Returns: + List of instances, ex: + ["hub/group/project"] -> ["hub", "hub/project", "hub/project/group"] + """ + network = self._get_network(self.access_token) + instances = self._get_instances_from_network(network) + + # Initially the list generated should not contain duplicates + # but this is just to sanitize the entry + logger.debug("Remove duplicates from instances") + return remove_duplicates_from_list(instances) diff --git a/gateway/api/use_cases/__init__.py b/gateway/api/use_cases/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/use_cases/authentication.py b/gateway/api/use_cases/authentication.py new file mode 100644 index 000000000..adb79b397 --- /dev/null +++ b/gateway/api/use_cases/authentication.py @@ -0,0 +1,46 @@ +"""Authentication use case to manage the authentication process in the api.""" + +import logging +from django.contrib.auth.models import AbstractUser + +from api.repositories.users import UserRepository +from api.services.authentication.quantum_platform import QuantumPlatformService + + +logger = logging.getLogger("gateway.use_cases.authentication") + + +class AuthenticationUseCase: # pylint: disable=too-few-public-methods + """ + This class will manage the authentication flow for the api. + """ + + user_repository = UserRepository() + + def __init__(self, authorization_token: str): + self.authorization_token = authorization_token + + def execute(self) -> type[AbstractUser] | None: + """ + This contains the logic to authenticate and validate the user + that is doing the request. + """ + quantum_platform_service = QuantumPlatformService( + authorization_token=self.authorization_token + ) + user_id = quantum_platform_service.authenticate() + if user_id is None: + return None + + verified = quantum_platform_service.verify_access() + if verified is False: + return None + + access_groups = quantum_platform_service.get_groups() + + quantum_user = self.user_repository.get_or_create_by_id(user_id=user_id) + self.user_repository.restart_user_groups( + user=quantum_user, unique_group_names=access_groups + ) + + return quantum_user diff --git a/gateway/main/settings.py b/gateway/main/settings.py index 7229594cc..622ed7c15 100644 --- a/gateway/main/settings.py +++ b/gateway/main/settings.py @@ -293,16 +293,7 @@ } # custom token auth -SETTINGS_TOKEN_AUTH_URL = os.environ.get("SETTINGS_TOKEN_AUTH_URL", None) -SETTINGS_TOKEN_AUTH_USER_FIELD = os.environ.get( - "SETTINGS_TOKEN_AUTH_USER_FIELD", "userId" -) -SETTINGS_TOKEN_AUTH_TOKEN_FIELD = os.environ.get( - "SETTINGS_TOKEN_AUTH_TOKEN_FIELD", "apiToken" -) -SETTINGS_TOKEN_AUTH_VERIFICATION_URL = os.environ.get( - "SETTINGS_TOKEN_AUTH_VERIFICATION_URL", None -) +QUANTUM_PLATFORM_API_BASE_URL = os.environ.get("QUANTUM_PLATFORM_API_BASE_URL", None) # verification fields to check when returned from auth api # Example of checking multiple fields: # For following verification data diff --git a/gateway/tests/api/test_authentication.py b/gateway/tests/api/test_authentication.py index 5b06c7aaa..49817bab3 100644 --- a/gateway/tests/api/test_authentication.py +++ b/gateway/tests/api/test_authentication.py @@ -6,7 +6,7 @@ from rest_framework.test import APITestCase from api.authentication import CustomTokenBackend, CustomToken, MockAuthBackend -from api.models_proxies import QuantumUserProxy +from api.services.authentication.quantum_platform import QuantumPlatformService class TestAuthentication(APITestCase): @@ -24,7 +24,7 @@ class TestAuthentication(APITestCase): ] @responses.activate - @patch.object(QuantumUserProxy, "_get_network") + @patch.object(QuantumPlatformService, "_get_network") def test_custom_token_authentication(self, get_network_mock: MagicMock): """Tests custom token auth.""" @@ -32,14 +32,14 @@ def test_custom_token_authentication(self, get_network_mock: MagicMock): responses.add( responses.POST, - "http://token_auth_url", + "http://token_auth_url/api/users/loginWithToken", json={"userId": "AwesomeUser", "id": "requestId"}, status=200, ) responses.add( responses.GET, - "http://token_auth_verification_url", + "http://token_auth_url/api/users/me", json={"is_valid": True}, status=200, ) @@ -49,9 +49,7 @@ def test_custom_token_authentication(self, get_network_mock: MagicMock): request.META.get.return_value = "Bearer AWESOME_TOKEN" with self.settings( - SETTINGS_TOKEN_AUTH_URL="http://token_auth_url", - SETTINGS_TOKEN_AUTH_USER_FIELD="userId", - SETTINGS_TOKEN_AUTH_VERIFICATION_URL="http://token_auth_verification_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid", ): user, token = custom_auth.authenticate(request) @@ -69,14 +67,14 @@ def test_with_nested_verification_fields(self): """Tests custom token auth.""" responses.add( responses.POST, - "http://token_auth_url", + "http://token_auth_url/api/users/loginWithToken", json={"userId": "AwesomeUser", "id": "requestId"}, status=200, ) responses.add( responses.GET, - "http://token_auth_verification_url", + "http://token_auth_url/api/users/me", json={"is_valid": True, "other": {"nested": {"field": "something_here"}}}, status=200, ) @@ -86,9 +84,7 @@ def test_with_nested_verification_fields(self): request.META.get.return_value = "Bearer AWESOME_TOKEN" with self.settings( - SETTINGS_TOKEN_AUTH_URL="http://token_auth_url", - SETTINGS_TOKEN_AUTH_USER_FIELD="userId", - SETTINGS_TOKEN_AUTH_VERIFICATION_URL="http://token_auth_verification_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,nested,field", ): user, token = custom_auth.authenticate(request) @@ -99,9 +95,7 @@ def test_with_nested_verification_fields(self): self.assertEqual(user.username, "AwesomeUser") with self.settings( - SETTINGS_TOKEN_AUTH_URL="http://token_auth_url", - SETTINGS_TOKEN_AUTH_USER_FIELD="userId", - SETTINGS_TOKEN_AUTH_VERIFICATION_URL="http://token_auth_verification_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,WRONG_NESTED_FIELD", ): user, token = custom_auth.authenticate(request) @@ -111,15 +105,13 @@ def test_with_nested_verification_fields(self): responses.add( responses.GET, - "http://token_auth_verification_url", + "http://token_auth_url/api/users/me", json={"is_valid": True, "other": "no nested fields"}, status=200, ) with self.settings( - SETTINGS_TOKEN_AUTH_URL="http://token_auth_url", - SETTINGS_TOKEN_AUTH_USER_FIELD="userId", - SETTINGS_TOKEN_AUTH_VERIFICATION_URL="http://token_auth_verification_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,nested,field", ): # this should raise an error as `SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD` diff --git a/gateway/tests/api/test_models_proxies.py b/gateway/tests/api/test_models_proxies.py deleted file mode 100644 index 6b417e335..000000000 --- a/gateway/tests/api/test_models_proxies.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Tests for proxies.""" - -from django.contrib.auth.models import Group, Permission -from django.db.models import Q -from django.urls import reverse -from rest_framework.test import APITestCase -from unittest.mock import MagicMock, patch - -from api.models import VIEW_PROGRAM_PERMISSION, Program -from api.models_proxies import QuantumUserProxy - - -class ProxiesTest(APITestCase): - """Tests for proxies.""" - - fixtures = ["tests/fixtures/acl_fixtures.json"] - - network_configuration = [ - { - "name": "ibm-q", - "groups": { - "open": { - "name": "open", - "projects": { - "main": { - "name": "main", - } - }, - } - }, - } - ] - - network_configuration_without_project = [ - { - "name": "ibm-q", - "groups": { - "open": { - "name": "open", - } - }, - } - ] - - def test_query_to_view_test_user_programs(self): - groups_id_for_view = [100, 101, 102, 103, 104] - view_program_permission = Permission.objects.get( - codename=VIEW_PROGRAM_PERMISSION - ) - - # We give view permission to all groups - for group_id_to_view in groups_id_for_view: - group = Group.objects.get(pk=group_id_to_view) - group.permissions.add(view_program_permission) - - # Call to program-list end-point - test_user_proxy = QuantumUserProxy.objects.get(username="test_user") - self.client.force_authenticate(user=test_user_proxy) - response = self.client.get(reverse("v1:programs-list"), format="json") - titles_from_response = [] - for program in response.data: - titles_from_response.append(program["title"]) - - programs_to_test = ["Public program", "Private program", "My program"] - self.assertListEqual(titles_from_response, programs_to_test) - - def test_query_to_view_test_user_2_programs(self): - groups_id_for_view = [100, 101, 102, 103, 104] - view_program_permission = Permission.objects.get( - codename=VIEW_PROGRAM_PERMISSION - ) - - # We give view permission to all groups - for group_id_to_view in groups_id_for_view: - group = Group.objects.get(pk=group_id_to_view) - group.permissions.add(view_program_permission) - - # Test user groups with view permissions - test_user_2_proxy = QuantumUserProxy.objects.get(username="test_user_2") - self.client.force_authenticate(user=test_user_2_proxy) - response = self.client.get(reverse("v1:programs-list"), format="json") - titles_from_response = [] - for program in response.data: - titles_from_response.append(program["title"]) - - programs_to_test = ["Public program"] - self.assertListEqual(titles_from_response, programs_to_test) - - def test_query_to_run_test_user_programs(self): - groups_id_for_run = [102, 104] - run_program_permission = Permission.objects.get(codename="run_program") - - # We give run permission to specific groups - for group_id_to_run in groups_id_for_run: - group = Group.objects.get(pk=group_id_to_run) - group.permissions.add(run_program_permission) - - program_with_run_permission = Program.objects.get(title="Private program") - program_without_run_permission = Program.objects.get(title="Public program") - - test_user_proxy = QuantumUserProxy.objects.get(username="test_user") - user_criteria = Q(user=test_user_proxy) - run_permission_criteria = Q(permissions=run_program_permission) - program_pk_criteria = Q(program_instances=program_with_run_permission) - test_user_program_with_run_permissions = Group.objects.filter( - user_criteria & run_permission_criteria & program_pk_criteria - ) - self.assertTrue(test_user_program_with_run_permissions.exists()) - - program_pk_criteria = Q(program_instances=program_without_run_permission) - test_user_program_without_run_permissions = Group.objects.filter( - user_criteria & run_permission_criteria & program_pk_criteria - ) - self.assertFalse(test_user_program_without_run_permissions.exists()) - - def test_instances_generation_from_quantum_network_with_projects(self): - proxy = QuantumUserProxy() - instances = proxy._get_instances_from_network(self.network_configuration) - self.assertListEqual(instances, ["ibm-q", "ibm-q/open", "ibm-q/open/main"]) - - def test_instances_generation_from_quantum_network_without_projects(self): - proxy = QuantumUserProxy() - instances = proxy._get_instances_from_network( - self.network_configuration_without_project - ) - self.assertListEqual(instances, ["ibm-q", "ibm-q/open"]) - - @patch.object(QuantumUserProxy, "_get_network") - def test_user_is_assigned_to_groups(self, get_network_mock: MagicMock): - get_network_mock.return_value = self.network_configuration - proxy = QuantumUserProxy.objects.get(username="test_user_3") - proxy.update_groups("") - - groups_names = proxy.groups.values_list("name", flat=True).distinct() - groups_names_list = list(groups_names) - self.assertListEqual( - groups_names_list, ["ibm-q", "ibm-q/open", "ibm-q/open/main"] - ) - - permissions = proxy.get_group_permissions() - permissions_list = list(permissions) - self.assertListEqual(permissions_list, ["api.view_program"]) From 42697c34b9573f971085e2aee825502baecaa5e5 Mon Sep 17 00:00:00 2001 From: David <9059044+Tansito@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:14:42 -0500 Subject: [PATCH 2/5] not include api end-point as in IQP env --- gateway/api/services/authentication/quantum_platform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway/api/services/authentication/quantum_platform.py b/gateway/api/services/authentication/quantum_platform.py index 323cfb0c3..95478df8c 100644 --- a/gateway/api/services/authentication/quantum_platform.py +++ b/gateway/api/services/authentication/quantum_platform.py @@ -19,9 +19,9 @@ class QuantumPlatformService: def __init__(self, authorization_token): self.auth_url = ( - f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/api/users/loginWithToken" + f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/users/loginWithToken" ) - self.verification_url = f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/api/users/me" + self.verification_url = f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/users/me" self.instances_url = f"{settings.IQP_QCON_API_BASE_URL}/network" self.authorization_token = authorization_token self.access_token = None From 8854265cde34177c4e878445add39b67d37ad652 Mon Sep 17 00:00:00 2001 From: David <9059044+Tansito@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:18:55 -0500 Subject: [PATCH 3/5] fix black --- gateway/api/services/authentication/quantum_platform.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gateway/api/services/authentication/quantum_platform.py b/gateway/api/services/authentication/quantum_platform.py index 95478df8c..abf995884 100644 --- a/gateway/api/services/authentication/quantum_platform.py +++ b/gateway/api/services/authentication/quantum_platform.py @@ -18,9 +18,7 @@ class QuantumPlatformService: """ def __init__(self, authorization_token): - self.auth_url = ( - f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/users/loginWithToken" - ) + self.auth_url = f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/users/loginWithToken" self.verification_url = f"{settings.QUANTUM_PLATFORM_API_BASE_URL}/users/me" self.instances_url = f"{settings.IQP_QCON_API_BASE_URL}/network" self.authorization_token = authorization_token From 8a6d12707c7c55e3db4af731dad0d3d3e28bb17b Mon Sep 17 00:00:00 2001 From: David <9059044+Tansito@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:31:03 -0500 Subject: [PATCH 4/5] fix test with last changes --- gateway/tests/api/test_authentication.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gateway/tests/api/test_authentication.py b/gateway/tests/api/test_authentication.py index 49817bab3..f1cd72a2d 100644 --- a/gateway/tests/api/test_authentication.py +++ b/gateway/tests/api/test_authentication.py @@ -49,7 +49,7 @@ def test_custom_token_authentication(self, get_network_mock: MagicMock): request.META.get.return_value = "Bearer AWESOME_TOKEN" with self.settings( - QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url/api", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid", ): user, token = custom_auth.authenticate(request) @@ -84,7 +84,7 @@ def test_with_nested_verification_fields(self): request.META.get.return_value = "Bearer AWESOME_TOKEN" with self.settings( - QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url/api", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,nested,field", ): user, token = custom_auth.authenticate(request) @@ -95,7 +95,7 @@ def test_with_nested_verification_fields(self): self.assertEqual(user.username, "AwesomeUser") with self.settings( - QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url/api", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,WRONG_NESTED_FIELD", ): user, token = custom_auth.authenticate(request) @@ -111,7 +111,7 @@ def test_with_nested_verification_fields(self): ) with self.settings( - QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url", + QUANTUM_PLATFORM_API_BASE_URL="http://token_auth_url/api", SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD="is_valid;other,nested,field", ): # this should raise an error as `SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD` From d779f7e6d7ddba235211e9602060d0bf2666acc0 Mon Sep 17 00:00:00 2001 From: David <9059044+Tansito@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:10:29 -0500 Subject: [PATCH 5/5] fix bug token is none --- gateway/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/api/authentication.py b/gateway/api/authentication.py index 18fed16c5..21374de65 100644 --- a/gateway/api/authentication.py +++ b/gateway/api/authentication.py @@ -34,7 +34,7 @@ def authenticate(self, request): logger.warning( "Problems authenticating: user did not provide authorization token." ) - return quantum_user, CustomToken(authorization_token.encode()) + return quantum_user, authorization_token authorization_token = auth_header.split(" ")[-1] quantum_user = AuthenticationUseCase(