Skip to content

Commit

Permalink
Authentication logic refactor (#1586)
Browse files Browse the repository at this point in the history
* Authentication logic refactor

* not include api end-point as in IQP env

* fix black

* fix test with last changes

* fix bug token is none
  • Loading branch information
Tansito authored Feb 24, 2025
1 parent 099bd2e commit dee2888
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 366 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ docker build -t qiskit/qiskit-serverless/gateway:<VERSION> .
| 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 |
Expand Down
87 changes: 14 additions & 73 deletions gateway/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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, authorization_token
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):
Expand Down
114 changes: 0 additions & 114 deletions gateway/api/models_proxies.py

This file was deleted.

54 changes: 52 additions & 2 deletions gateway/api/repositories/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Empty file.
Loading

0 comments on commit dee2888

Please sign in to comment.