wip
This commit is contained in:
@@ -156,14 +156,14 @@ class RecordingAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class ServiceAccountDomainInline(admin.TabularInline):
|
||||
"""Wip."""
|
||||
"""Inline admin for managing allowed domains per service account."""
|
||||
|
||||
model = models.ServiceAccountDomain
|
||||
extra = 0
|
||||
|
||||
|
||||
class ServiceAccountAdminForm(forms.ModelForm):
|
||||
"""Wip."""
|
||||
"""Custom form for ServiceAccount admin with multi-select scopes."""
|
||||
|
||||
scopes = forms.MultipleChoiceField(
|
||||
choices=models.ServiceAccountScope.choices,
|
||||
@@ -179,7 +179,7 @@ class ServiceAccountAdminForm(forms.ModelForm):
|
||||
|
||||
@admin.register(models.ServiceAccount)
|
||||
class ServiceAccountAdmin(admin.ModelAdmin):
|
||||
"""Wip."""
|
||||
"""Admin interface for managing service accounts and their permissions."""
|
||||
|
||||
form = ServiceAccountAdminForm
|
||||
|
||||
@@ -198,8 +198,8 @@ class ServiceAccountAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(models.ServiceAccountAPIKey)
|
||||
class OrganizationAPIKeyModelAdmin(APIKeyModelAdmin):
|
||||
"""Wip."""
|
||||
class ServiceAccountAPIKeyModelAdmin(APIKeyModelAdmin):
|
||||
"""Admin interface for managing service account API keys."""
|
||||
|
||||
list_display = [*APIKeyModelAdmin.list_display, "service_account__name"]
|
||||
search_fields = [*APIKeyModelAdmin.search_fields, "service_account__name"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Meet core external API endpoints"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Wip."""
|
||||
"""Authentication Backends for service accounts integrated to the Meet core app."""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -14,13 +14,11 @@ User = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrationJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""
|
||||
Simple JWT authentication for external-api endpoints.
|
||||
"""
|
||||
class ServiceAccountJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""JWT authentication for external API endpoints."""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Wip."""
|
||||
"""Extract and validate JWT from Authorization header."""
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
|
||||
@@ -28,19 +26,19 @@ class IntegrationJWTAuthentication(authentication.BaseAuthentication):
|
||||
return None
|
||||
|
||||
if len(auth_header) != 2:
|
||||
logger.error("Invalid token header")
|
||||
logger.warning("Invalid token header")
|
||||
raise exceptions.AuthenticationFailed("Invalid token header.")
|
||||
|
||||
try:
|
||||
token = auth_header[1].decode("utf-8")
|
||||
except UnicodeError as e:
|
||||
logger.error("Invalid: %s", e)
|
||||
logger.warning("Invalid: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
"""Wip."""
|
||||
"""Authenticate and validate JWT token credentials."""
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
@@ -50,29 +48,31 @@ class IntegrationJWTAuthentication(authentication.BaseAuthentication):
|
||||
issuer=settings.INTEGRATIONS_JWT_ISSUER,
|
||||
)
|
||||
except jwt.ExpiredSignatureError as e:
|
||||
logger.error("Token expired")
|
||||
logger.warning("Token expired")
|
||||
raise exceptions.AuthenticationFailed("Token expired.") from e
|
||||
except jwt.InvalidIssuerError as e:
|
||||
logger.error("Invalid JWT issuer: %s", e)
|
||||
logger.warning("Invalid JWT issuer: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.error("Invalid JWT token: %s", e)
|
||||
logger.warning("Invalid JWT token: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
|
||||
if not payload.get("user_id"):
|
||||
logger.warning("Invalid JWT token. Missing 'user_id' in payload")
|
||||
return None
|
||||
user_id = payload.get("user_id")
|
||||
if not user_id:
|
||||
logger.warning("Missing 'user_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token.")
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=payload["user_id"])
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist as e:
|
||||
logger.warning("User not found")
|
||||
raise exceptions.AuthenticationFailed("User not found.") from e
|
||||
logger.warning("User not found: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
|
||||
if not user.is_active:
|
||||
logger.warning("User inactive")
|
||||
raise exceptions.AuthenticationFailed("User inactive.")
|
||||
logger.warning("Inactive user attempted authentication: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("Account disabled.")
|
||||
|
||||
# Attach token metadata to user
|
||||
user.token_scopes = payload.get("scope", [])
|
||||
user.is_impersonated = payload.get("impersonated", False)
|
||||
user.client_id = payload.get("client_id")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Wip."""
|
||||
"""Permission handlers for the external API of the Meet core app."""
|
||||
|
||||
from rest_framework import exceptions, permissions
|
||||
from rest_framework_api_key.permissions import BaseHasAPIKey
|
||||
@@ -7,23 +7,26 @@ from ..models import ServiceAccountAPIKey, ServiceAccountScope
|
||||
|
||||
|
||||
class HasServiceAccountAPIKey(BaseHasAPIKey):
|
||||
"""Wip."""
|
||||
"""Permission class that requires a valid service account API key.
|
||||
|
||||
Validates that the request includes a valid API key in the Authorization header
|
||||
using the format: "Api-Key <key>". The API key must belong to an active
|
||||
ServiceAccount and the key itself must not be revoked.
|
||||
|
||||
This permission is typically used for server-to-server authentication
|
||||
where external services need to authenticate with our API."""
|
||||
|
||||
model = ServiceAccountAPIKey
|
||||
|
||||
|
||||
class IsAuthenticated(permissions.BasePermission):
|
||||
"""
|
||||
Allows access only to authenticated users. Alternative method checking the presence
|
||||
of the auth token to avoid hitting the database.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return bool(request.auth) or request.user.is_authenticated
|
||||
|
||||
|
||||
class HasRequiredScope(permissions.BasePermission):
|
||||
"""Check if JWT token has required scope for the action."""
|
||||
"""Enforces scope-based access control on ViewSet actions.
|
||||
|
||||
Provides fine-grained permission management by mapping ViewSet actions
|
||||
to required scopes. JWT tokens must contain the appropriate scope to
|
||||
perform specific operations, enabling precise control over what external
|
||||
services can access (principle of least privilege).
|
||||
"""
|
||||
|
||||
# Map ViewSet actions to required scopes
|
||||
scope_map = {
|
||||
@@ -36,7 +39,7 @@ class HasRequiredScope(permissions.BasePermission):
|
||||
}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Wip."""
|
||||
"""Check if JWT token contains required scope for this action."""
|
||||
|
||||
action = getattr(view, "action", None)
|
||||
if not action:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Wip."""
|
||||
"""Client serializers for the external API of the Meet core app."""
|
||||
|
||||
# pylint: disable=abstract-method,no-name-in-module
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -17,7 +17,17 @@ class JwtSerializer(BaseValidationOnlySerializer):
|
||||
|
||||
|
||||
class RoomSerializer(serializers.ModelSerializer):
|
||||
"""Serialize Room model for the API."""
|
||||
"""External API serializer with limited, safe room information.
|
||||
|
||||
Designed for external integrations with:
|
||||
- Read-only access to most fields for security
|
||||
- Additional computed fields (url, telephony info)
|
||||
- Filtered data appropriate for external consumption
|
||||
- Automatic room creation with secure defaults
|
||||
|
||||
Intentionally limits exposed information compared to internal APIs,
|
||||
providing only what external services need while protecting sensitive details.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = models.Room
|
||||
@@ -25,7 +35,7 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ["id", "name", "slug", "pin_code", "access_level"]
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""Wip."""
|
||||
"""Add external-specific fields and filter sensitive data."""
|
||||
output = super().to_representation(instance)
|
||||
request = self.context.get("request")
|
||||
|
||||
@@ -46,10 +56,8 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
return output
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Custom create method."""
|
||||
|
||||
"""Create room with secure defaults for external API."""
|
||||
# todo - track source of creation
|
||||
|
||||
validated_data["name"] = utils.generate_slug()
|
||||
validated_data["access_level"] = models.RoomAccessLevel.TRUSTED
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Wip."""
|
||||
"""External API endpoints"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
@@ -17,18 +17,15 @@ from rest_framework import (
|
||||
status as drf_status,
|
||||
)
|
||||
|
||||
from core import models
|
||||
from core import api, models
|
||||
|
||||
from . import permissions, serializers
|
||||
from .authentication import IntegrationJWTAuthentication
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
from . import authentication, permissions, serializers
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrationViewSet(viewsets.GenericViewSet):
|
||||
"""Wip."""
|
||||
class ServiceAccountViewSet(viewsets.GenericViewSet):
|
||||
"""Service account management for external integrations."""
|
||||
|
||||
permission_classes = [permissions.HasServiceAccountAPIKey]
|
||||
|
||||
@@ -39,7 +36,14 @@ class IntegrationViewSet(viewsets.GenericViewSet):
|
||||
url_name="token",
|
||||
)
|
||||
def generate_token(self, request, *args, **kwargs):
|
||||
"""Generate JWT token for a specific user identified by email."""
|
||||
"""Generate JWT token for user impersonation.
|
||||
|
||||
Token generation endpoint for service-to-service authentication.
|
||||
|
||||
Exchanges service account API key for a scoped JWT token that can
|
||||
impersonate a specific user. The service account must have permission
|
||||
to impersonate the user's email domain.
|
||||
"""
|
||||
|
||||
serializer = serializers.JwtSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
@@ -81,7 +85,7 @@ class IntegrationViewSet(viewsets.GenericViewSet):
|
||||
"iss": settings.INTEGRATIONS_JWT_ISSUER,
|
||||
"impersonated": True,
|
||||
"client_id": str(service_account.id), # audience
|
||||
"scope": service_account.scopes,
|
||||
"scope": service_account.scopes or [],
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -112,10 +116,14 @@ class RoomViewSet(
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""Wip."""
|
||||
"""External API for room management with scope-based permissions.
|
||||
|
||||
authentication_classes = [IntegrationJWTAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.HasRequiredScope]
|
||||
Provides JWT-authenticated access to room operations for external services.
|
||||
All operations are scoped and filtered to authenticated user's accessible rooms.
|
||||
"""
|
||||
|
||||
authentication_classes = [authentication.ServiceAccountJWTAuthentication]
|
||||
permission_classes = [api.permissions.IsAuthenticated, permissions.HasRequiredScope]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ external_router.register(
|
||||
"rooms", external_viewsets.RoomViewSet, basename="external_rooms"
|
||||
)
|
||||
external_router.register(
|
||||
"integrations",
|
||||
external_viewsets.IntegrationViewSet,
|
||||
basename="external_integrations",
|
||||
"service-account",
|
||||
external_viewsets.ServiceAccountViewSet,
|
||||
basename="external_service_account",
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -30,7 +30,7 @@ from livekit.api import ( # pylint: disable=E0611
|
||||
|
||||
|
||||
def generate_slug():
|
||||
"""Wip."""
|
||||
"""Generate a random room slug in the format 'xxx-xxxx-xxx'."""
|
||||
|
||||
sizes = [3, 4, 3]
|
||||
parts = [
|
||||
|
||||
Reference in New Issue
Block a user