✨(core) create AccountServiceAuthentication backend
Backend authentication with API Key to AccountService
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- ✨(core) create AccountServiceAuthentication backend #771
|
||||||
- ✨(core) create AccountService model #771
|
- ✨(core) create AccountService model #771
|
||||||
- 🧱(helm) disable createsuperuser job by setting #863
|
- 🧱(helm) disable createsuperuser job by setting #863
|
||||||
- 🔒️(passwords) add validators for production #850
|
- 🔒️(passwords) add validators for production #850
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import requests
|
|||||||
from mozilla_django_oidc.auth import (
|
from mozilla_django_oidc.auth import (
|
||||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||||
)
|
)
|
||||||
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from core.models import (
|
from core.models import (
|
||||||
|
AccountService,
|
||||||
Contact,
|
Contact,
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationAccess,
|
OrganizationAccess,
|
||||||
@@ -245,3 +248,45 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
|
|
||||||
if updated_claims:
|
if updated_claims:
|
||||||
self.UserModel.objects.filter(sub=user.sub).update(**updated_claims)
|
self.UserModel.objects.filter(sub=user.sub).update(**updated_claims)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountServiceAuthentication(BaseAuthentication):
|
||||||
|
"""Authentication backend for account services using Authorization header.
|
||||||
|
The Authorization header is used to authenticate the request.
|
||||||
|
The api key is stored in the AccountService model.
|
||||||
|
|
||||||
|
Header format:
|
||||||
|
Authorization: ApiKey <api_key>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
"""Authenticate the request. Find the account service and check the api key.
|
||||||
|
|
||||||
|
Should return either:
|
||||||
|
- a tuple of (account_service, api_key) if allowed,
|
||||||
|
- None to pass on other authentication backends
|
||||||
|
- raise an AuthenticationFailed exception to stop propagation.
|
||||||
|
"""
|
||||||
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
if not auth_header:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
auth_mode, api_key = auth_header.split(" ")
|
||||||
|
except (IndexError, ValueError) as err:
|
||||||
|
raise AuthenticationFailed(_("Invalid authorization header.")) from err
|
||||||
|
if auth_mode.lower() != "apikey" or not api_key:
|
||||||
|
raise AuthenticationFailed(_("Invalid authorization header."))
|
||||||
|
try:
|
||||||
|
account_service = AccountService.objects.get(api_key=api_key)
|
||||||
|
except AccountService.DoesNotExist as err:
|
||||||
|
logger.warning("Invalid api_key: %s", api_key)
|
||||||
|
raise AuthenticationFailed(_("Invalid api key.")) from err
|
||||||
|
return (account_service, account_service.api_key)
|
||||||
|
|
||||||
|
def authenticate_header(self, request):
|
||||||
|
"""
|
||||||
|
Return a string to be used as the value of the `WWW-Authenticate`
|
||||||
|
header in a `401 Unauthenticated` response, or `None` if the
|
||||||
|
authentication scheme should return `403 Permission Denied` responses.
|
||||||
|
"""
|
||||||
|
return "apikey"
|
||||||
|
|||||||
@@ -1122,3 +1122,8 @@ class AccountService(BaseModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
"""Indicate if the account service is authenticated."""
|
||||||
|
return True
|
||||||
|
|||||||
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
from django.test import RequestFactory, override_settings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from core import factories, models
|
from core import factories, models
|
||||||
from core.authentication.backends import OIDCAuthenticationBackend
|
from core.authentication.backends import (
|
||||||
|
AccountServiceAuthentication,
|
||||||
|
OIDCAuthenticationBackend,
|
||||||
|
)
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@@ -453,3 +458,60 @@ def test_authentication_getter_existing_user_with_registration_id(
|
|||||||
|
|
||||||
assert user.organization is not None
|
assert user.organization is not None
|
||||||
assert user.organization.registration_id_list == ["12345678901234"]
|
assert user.organization.registration_id_list == ["12345678901234"]
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ACCOUNT_SERVICE_SCOPES=["la-suite-list-organizations-siret"])
|
||||||
|
def test_account_service_authenticate_valid_api_key():
|
||||||
|
"""Test the authenticate method with a valid API key."""
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
account_service = factories.AccountServiceFactory(
|
||||||
|
name="test_service",
|
||||||
|
api_key="test_api_key_123",
|
||||||
|
scopes=["la-suite-list-organizations-siret"],
|
||||||
|
)
|
||||||
|
request.headers = {"Authorization": f"ApiKey {account_service.api_key}"}
|
||||||
|
|
||||||
|
result = AccountServiceAuthentication().authenticate(request)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result[0] == account_service
|
||||||
|
assert result[1] == account_service.api_key
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_service_authenticate_missing_api_key():
|
||||||
|
"""Test the authenticate method with a missing API key."""
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
request.headers = {}
|
||||||
|
|
||||||
|
result = AccountServiceAuthentication().authenticate(request)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_service_authenticate_invalid_api_key():
|
||||||
|
"""Test the authenticate method with an invalid API key."""
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
request.headers = {"Authorization": "ApiKey invalid_key"}
|
||||||
|
|
||||||
|
with pytest.raises(AuthenticationFailed):
|
||||||
|
AccountServiceAuthentication().authenticate(request)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ACCOUNT_SERVICE_SCOPES=["la-suite-list-organizations-siret"])
|
||||||
|
def test_account_service_authenticate_invalid_header():
|
||||||
|
"""Test the authenticate method with an invalid header."""
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
account_service = factories.AccountServiceFactory(
|
||||||
|
name="test_service",
|
||||||
|
api_key="test_api_key_123",
|
||||||
|
scopes=["la-suite-list-organizations-siret"],
|
||||||
|
)
|
||||||
|
request.headers = {"Authorization": f"Bearer {account_service.api_key}"}
|
||||||
|
|
||||||
|
with pytest.raises(AuthenticationFailed):
|
||||||
|
AccountServiceAuthentication().authenticate(request)
|
||||||
|
|
||||||
|
request.headers = {"Authorization": account_service.api_key}
|
||||||
|
|
||||||
|
with pytest.raises(AuthenticationFailed):
|
||||||
|
AccountServiceAuthentication().authenticate(request)
|
||||||
|
|||||||
Reference in New Issue
Block a user