Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication logic refactor #1586

Merged
merged 5 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 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):
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]:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also found a way to type that we are returning a user with type[AbstractUser]. This way the linter doesn't complain 😂

"""
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