mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-12 11:46:52 +00:00
🔧(backend) add setting to toggle application token exchange mechanism
Introduce a configuration flag to enable or disable the application token exchange (service account) mechanism. This allows activating alternative authentication backends without requiring full application token configuration. Required to support the upcoming add-ons authentication backend.
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to
|
||||
- ✨(backend) introduce add-ons authentication backend
|
||||
- 💬(backend) clarify french transcription audio download link text #1299
|
||||
- 🚧(addons) introduce initial Microsoft Outlook add-in support (alpha)
|
||||
- 🔧(backend) add setting to toggle application token exchange mechanism
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class FeatureFlag:
|
||||
"subtitle": "ROOM_SUBTITLE_ENABLED",
|
||||
"file_upload": "FILE_UPLOAD_ENABLED",
|
||||
"addons": "ADDONS_ENABLED",
|
||||
"application": "APPLICATION_ENABLED",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -23,7 +23,14 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""Base JWT authentication class."""
|
||||
|
||||
def __init__(
|
||||
self, secret_key, algorithm, issuer, audience, expiration_seconds, token_type
|
||||
self,
|
||||
secret_key,
|
||||
algorithm,
|
||||
issuer,
|
||||
audience,
|
||||
expiration_seconds,
|
||||
token_type,
|
||||
is_enabled,
|
||||
):
|
||||
"""Initialize the JWT authentication backend with the given token service configuration.
|
||||
|
||||
@@ -34,10 +41,17 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
audience: Expected token audience identifier
|
||||
expiration_seconds: Token expiration time in seconds
|
||||
token_type: Token type (e.g. Bearer)
|
||||
is_enabled: Whether this authentication backend is active
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.is_enabled = is_enabled
|
||||
self._token_service = None
|
||||
|
||||
if not self.is_enabled:
|
||||
return
|
||||
|
||||
self._token_service = jwt_token.JwtTokenService(
|
||||
secret_key=secret_key,
|
||||
algorithm=algorithm,
|
||||
@@ -54,6 +68,9 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
Tuple of (user, payload) if authentication successful, None otherwise
|
||||
"""
|
||||
|
||||
if not self.is_enabled:
|
||||
return None
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
|
||||
if not auth_header or auth_header[0].lower() != b"bearer":
|
||||
@@ -186,6 +203,7 @@ class ApplicationJWTAuthentication(BaseJWTAuthentication):
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
expiration_seconds=settings.APPLICATION_JWT_EXPIRATION_SECONDS,
|
||||
token_type=settings.APPLICATION_JWT_TOKEN_TYPE,
|
||||
is_enabled=settings.APPLICATION_ENABLED,
|
||||
)
|
||||
|
||||
def validate_payload(self, payload):
|
||||
|
||||
@@ -20,6 +20,7 @@ from rest_framework import (
|
||||
)
|
||||
|
||||
from core import api, models
|
||||
from core.api.feature_flag import FeatureFlag
|
||||
from core.services.jwt_token import JwtTokenService
|
||||
|
||||
from . import authentication, permissions, serializers
|
||||
@@ -36,6 +37,7 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
url_path="token",
|
||||
url_name="token",
|
||||
)
|
||||
@FeatureFlag.require("application")
|
||||
def generate_jwt_access_token(self, request, *args, **kwargs):
|
||||
"""Generate JWT access token for application delegation.
|
||||
|
||||
|
||||
@@ -123,6 +123,25 @@ def test_api_rooms_list_with_expired_token(settings):
|
||||
assert "expired" in str(response.data).lower()
|
||||
|
||||
|
||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
||||
def test_api_rooms_list_with_application_disabled(mock_rs_authenticate, settings):
|
||||
"""Listing rooms should return 401 when application is disabled."""
|
||||
|
||||
settings.APPLICATION_ENABLED = False
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Generate expired token
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
mock_rs_authenticate.assert_called_once()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_list_with_invalid_rs_token(settings):
|
||||
"""Listing rooms with invalid resource server token should return 400."""
|
||||
@@ -1106,6 +1125,64 @@ def test_resource_server_authentication_successful(settings):
|
||||
assert expected_ids == results_id
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_authentication_successful_when_application_disabled(settings):
|
||||
"""Resource server should keep working when the application auth backend is disabled."""
|
||||
|
||||
settings.APPLICATION_ENABLED = False
|
||||
|
||||
user = UserFactory(sub="very-specific-sub")
|
||||
other_user = UserFactory()
|
||||
|
||||
RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
RoomFactory(access_level=RoomAccessLevel.TRUSTED)
|
||||
RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
room_user_accesses = RoomFactory(
|
||||
access_level=RoomAccessLevel.RESTRICTED, users=[user]
|
||||
)
|
||||
RoomFactory(access_level=RoomAccessLevel.RESTRICTED, users=[other_user])
|
||||
|
||||
assert (
|
||||
settings.OIDC_RS_BACKEND_CLASS
|
||||
== "core.external_api.authentication.ResourceServerBackend"
|
||||
)
|
||||
|
||||
settings.OIDC_RS_CLIENT_ID = "some_client_id"
|
||||
settings.OIDC_RS_CLIENT_SECRET = "some_client_secret"
|
||||
settings.OIDC_RS_SCOPES_PREFIX = "lasuite_meet"
|
||||
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
settings.OIDC_VERIFY_SSL = False
|
||||
settings.OIDC_TIMEOUT = 5
|
||||
settings.OIDC_PROXY = None
|
||||
settings.OIDC_OP_JWKS_ENDPOINT = "https://oidc.example.com/jwks"
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet lasuite_meet:rooms:list lasuite_meet:rooms:retrieve",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer some_token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 1
|
||||
expected_ids = {str(room_user_accesses.id)}
|
||||
results_id = {result["id"] for result in results}
|
||||
assert expected_ids == results_id
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_resource_server_denies_access_with_insufficient_scopes(settings):
|
||||
"""Requests should be denied when the token lacks required scopes.
|
||||
|
||||
@@ -19,6 +19,35 @@ from core.models import ApplicationScope, User
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_applications_generate_token_application_disabled(settings):
|
||||
"""When APPLICATION_ENABLED is False, the endpoint should return 404."""
|
||||
settings.APPLICATION_ENABLED = False
|
||||
|
||||
user = UserFactory(email="user@example.com")
|
||||
application = ApplicationFactory(
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST],
|
||||
)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
application.save()
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_api_applications_generate_token_success(settings):
|
||||
"""Valid credentials should return a JWT token."""
|
||||
UserFactory(email="User.Family@example.com")
|
||||
|
||||
@@ -819,6 +819,9 @@ class Base(Configuration):
|
||||
)
|
||||
|
||||
# External Applications
|
||||
APPLICATION_ENABLED = values.BooleanValue(
|
||||
False, environ_name="APPLICATION_ENABLED", environ_prefix=None
|
||||
)
|
||||
APPLICATION_CLIENT_ID_LENGTH = values.PositiveIntegerValue(
|
||||
40,
|
||||
environ_name="APPLICATION_CLIENT_ID_LENGTH",
|
||||
@@ -1079,6 +1082,7 @@ class Test(Base):
|
||||
"url": "http://127.0.0.1.nip.io:7880",
|
||||
}
|
||||
|
||||
APPLICATION_ENABLED = True
|
||||
APPLICATION_JWT_SECRET_KEY = "secret-key-padded-for-minimum-len!-application" # noqa:S105
|
||||
APPLICATION_JWT_AUDIENCE = "Test inc."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user