wip
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"""Permission handlers for the Meet core app."""
|
||||
|
||||
from rest_framework import permissions
|
||||
from rest_framework_api_key.permissions import BaseHasAPIKey
|
||||
|
||||
from ..models import RoleChoices, ServiceAccountAPIKey
|
||||
from ..models import RoleChoices
|
||||
|
||||
ACTION_FOR_METHOD_TO_PERMISSION = {
|
||||
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"}
|
||||
@@ -108,5 +107,3 @@ class HasLiveKitRoomAccess(permissions.BasePermission):
|
||||
return False
|
||||
return request.auth.video.room == str(obj.id)
|
||||
|
||||
class HasServiceAccountAPIKey(BaseHasAPIKey):
|
||||
model = ServiceAccountAPIKey
|
||||
|
||||
@@ -301,3 +301,47 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
) from e
|
||||
|
||||
return attrs
|
||||
|
||||
class IntegrationJwtSerializer(BaseValidationOnlySerializer):
|
||||
"""Validate room creation callback data."""
|
||||
|
||||
email = serializers.EmailField(required=True)
|
||||
|
||||
class IntegrationRoomSerializer(serializers.ModelSerializer):
|
||||
"""Serialize Room model for the API."""
|
||||
|
||||
class Meta:
|
||||
model = models.Room
|
||||
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
|
||||
read_only_fields = ["id", "slug", "pin_code"]
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Add users only for administrator users.
|
||||
Add LiveKit credentials for public instance or related users/groups
|
||||
"""
|
||||
output = super().to_representation(instance)
|
||||
request = self.context.get("request")
|
||||
|
||||
if not request:
|
||||
return output
|
||||
|
||||
role = instance.get_role(request.user)
|
||||
is_admin_or_owner = models.RoleChoices.check_administrator_role(
|
||||
role
|
||||
) or models.RoleChoices.check_owner_role(role)
|
||||
|
||||
if is_admin_or_owner:
|
||||
access_serializer = NestedResourceAccessSerializer(
|
||||
instance.accesses.select_related("resource", "user").all(),
|
||||
context=self.context,
|
||||
many=True,
|
||||
)
|
||||
output["accesses"] = access_serializer.data
|
||||
|
||||
if not is_admin_or_owner:
|
||||
del output["configuration"]
|
||||
|
||||
output["is_administrable"] = is_admin_or_owner
|
||||
|
||||
return output
|
||||
|
||||
@@ -863,21 +863,3 @@ class RecordingViewSet(
|
||||
request = utils.generate_s3_authorization_headers(recording.key)
|
||||
|
||||
return drf_response.Response("authorized", headers=request.headers, status=200)
|
||||
|
||||
class WipViewSet(viewsets.GenericViewSet):
|
||||
"""Wip."""
|
||||
|
||||
permission_classes = [permissions.HasServiceAccountAPIKey]
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
url_path="ping",
|
||||
url_name="ping",
|
||||
)
|
||||
def ping(self, request, *args, **kwargs):
|
||||
"""Wip."""
|
||||
return drf_response.Response(
|
||||
{"message": "pong"},
|
||||
status=drf_status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Wip."""
|
||||
|
||||
import logging
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IntegrationJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""
|
||||
Simple JWT authentication for external-api endpoints.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Wip."""
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
|
||||
if not auth_header or auth_header[0].lower() != b'bearer':
|
||||
return None
|
||||
|
||||
if len(auth_header) != 2:
|
||||
raise exceptions.AuthenticationFailed('Invalid token header.')
|
||||
|
||||
try:
|
||||
token = auth_header[1].decode('utf-8')
|
||||
except UnicodeError:
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
"""Wip."""
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.INTEGRATIONS_JWT_SECRET_KEY,
|
||||
algorithms=[settings.INTEGRATIONS_JWT_ALG],
|
||||
audience="wip"
|
||||
)
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.error("Token expired")
|
||||
raise exceptions.AuthenticationFailed('Token expired.')
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.error("Invalid JWT token: %s", e)
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
if not payload.get("user_id"):
|
||||
logger.warning("Invalid JWT token. Missing 'user_id' in payload")
|
||||
return None
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=payload['user_id'])
|
||||
except User.DoesNotExist:
|
||||
logger.warning("User not found")
|
||||
raise exceptions.AuthenticationFailed('User not found.')
|
||||
|
||||
if not user.is_active:
|
||||
logger.warning("User inactive")
|
||||
raise exceptions.AuthenticationFailed('User inactive.')
|
||||
return (user, token)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Wip."""
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
from rest_framework_api_key.permissions import BaseHasAPIKey
|
||||
|
||||
from ..models import ServiceAccountAPIKey
|
||||
|
||||
|
||||
|
||||
class HasServiceAccountAPIKey(BaseHasAPIKey):
|
||||
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
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Wip."""
|
||||
|
||||
# pylint: disable=abstract-method,no-name-in-module
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
from core import models, utils
|
||||
|
||||
from core.api.serializers import NestedResourceAccessSerializer, BaseValidationOnlySerializer
|
||||
|
||||
class JwtSerializer(BaseValidationOnlySerializer):
|
||||
"""Validate room creation callback data."""
|
||||
|
||||
email = serializers.EmailField(required=True)
|
||||
|
||||
class RoomSerializer(serializers.ModelSerializer):
|
||||
"""Serialize Room model for the API."""
|
||||
|
||||
class Meta:
|
||||
model = models.Room
|
||||
fields = ["id", "slug", "configuration", "pin_code", "access_level"]
|
||||
read_only_fields = ["id", "name", "slug", "pin_code", "access_level"]
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Add users only for administrator users.
|
||||
Add LiveKit credentials for public instance or related users/groups
|
||||
"""
|
||||
output = super().to_representation(instance)
|
||||
request = self.context.get("request")
|
||||
|
||||
if not request:
|
||||
return output
|
||||
|
||||
output["url"] = f"{settings.INTEGRATIONS_APP_BASE_URL}/{output["slug"]}"
|
||||
|
||||
if settings.ROOM_TELEPHONY_ENABLED:
|
||||
output["telephony"] = {
|
||||
"enabled": settings.ROOM_TELEPHONY_ENABLED,
|
||||
"pin_code": output["pin_code"],
|
||||
"phone_number": settings.ROOM_TELEPHONY_PHONE_NUMBER,
|
||||
"default_country": settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
|
||||
}
|
||||
del output["pin_code"]
|
||||
|
||||
return output
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Custom create method."""
|
||||
|
||||
# todo - extract this in an util function
|
||||
def generate_pattern():
|
||||
part1 = ''.join(random.choices(string.ascii_lowercase, k=3))
|
||||
part2 = ''.join(random.choices(string.ascii_lowercase, k=4))
|
||||
part3 = ''.join(random.choices(string.ascii_lowercase, k=3))
|
||||
return f"{part1}-{part2}-{part3}"
|
||||
|
||||
validated_data['name'] = generate_pattern()
|
||||
validated_data['access_level'] = "trusted"
|
||||
|
||||
return super().create(validated_data)
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Wip."""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from rest_framework import decorators, mixins, pagination, throttling, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
)
|
||||
from rest_framework import (
|
||||
response as drf_response,
|
||||
)
|
||||
from rest_framework import (
|
||||
status as drf_status,
|
||||
)
|
||||
|
||||
from core import enums, models, utils
|
||||
from .authentication import IntegrationJWTAuthentication
|
||||
from . import serializers, permissions
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrationViewSet(viewsets.GenericViewSet):
|
||||
"""Wip."""
|
||||
|
||||
permission_classes = [permissions.HasServiceAccountAPIKey]
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=["post"],
|
||||
url_path="token",
|
||||
url_name="token",
|
||||
)
|
||||
def generate_token(self, request, *args, **kwargs):
|
||||
"""Generate JWT token for a specific user identified by email."""
|
||||
|
||||
serializer = serializers.JwtSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
user = models.User.objects.get(email=serializer.validated_data["email"])
|
||||
except models.User.DoesNotExist:
|
||||
# todo - create unknown user
|
||||
raise drf_exceptions.NotFound({"error": "User with this email does not exist."})
|
||||
|
||||
now = datetime.utcnow()
|
||||
payload = {
|
||||
'user_id': str(user.id),
|
||||
'email': user.email,
|
||||
'full_name': user.full_name,
|
||||
'iat': now,
|
||||
'exp': now + timedelta(seconds=settings.INTEGRATIONS_JWT_EXPIRATION_SECONDS),
|
||||
'iss': settings.INTEGRATIONS_JWT_ISSUER,
|
||||
'aud': 'wip', # todo - replace with the owner of the api token
|
||||
# todo - add scope
|
||||
}
|
||||
|
||||
try:
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.INTEGRATIONS_JWT_SECRET_KEY,
|
||||
algorithm=settings.INTEGRATIONS_JWT_ALG
|
||||
)
|
||||
except Exception as e:
|
||||
return drf_response.Response(
|
||||
{"error": "Failed to generate token"},
|
||||
status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
return drf_response.Response(
|
||||
{
|
||||
"token": token,
|
||||
"token_type": settings.INTEGRATIONS_JWT_TOKEN_TYPE,
|
||||
"expires_in": settings.INTEGRATIONS_JWT_EXPIRATION_SECONDS,
|
||||
},
|
||||
status=drf_status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class RoomViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""Wip."""
|
||||
|
||||
authentication_classes = [IntegrationJWTAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
@@ -4,20 +4,24 @@ from django.conf import settings
|
||||
from django.urls import include, path
|
||||
|
||||
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.routers import DefaultRouter, SimpleRouter
|
||||
|
||||
from core.api import get_frontend_configuration, viewsets
|
||||
from core.external_api import viewsets as external_viewsets
|
||||
|
||||
# - Main endpoints
|
||||
router = DefaultRouter()
|
||||
router.register("users", viewsets.UserViewSet, basename="users")
|
||||
router.register("rooms", viewsets.RoomViewSet, basename="rooms")
|
||||
router.register("wip", viewsets.WipViewSet, basename="wip")
|
||||
router.register("recordings", viewsets.RecordingViewSet, basename="recordings")
|
||||
router.register(
|
||||
"resource-accesses", viewsets.ResourceAccessViewSet, basename="resource_accesses"
|
||||
)
|
||||
|
||||
external_router = DefaultRouter()
|
||||
external_router.register("rooms", external_viewsets.RoomViewSet, basename="external_rooms")
|
||||
external_router.register("integrations", external_viewsets.IntegrationViewSet, basename="external_integrations")
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/",
|
||||
@@ -29,4 +33,15 @@ urlpatterns = [
|
||||
]
|
||||
),
|
||||
),
|
||||
path(
|
||||
f"external-api/{settings.EXTERNAL_API_VERSION}/",
|
||||
include(
|
||||
[
|
||||
*external_router.urls,
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
print('core')
|
||||
print(external_router.urls)
|
||||
|
||||
@@ -69,6 +69,7 @@ class Base(Configuration):
|
||||
USE_SWAGGER = False
|
||||
|
||||
API_VERSION = "v1.0"
|
||||
EXTERNAL_API_VERSION = "v1.0"
|
||||
|
||||
DATA_DIR = values.Value(path.join("/", "data"), environ_name="DATA_DIR")
|
||||
|
||||
@@ -665,6 +666,35 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Integrations settings
|
||||
INTEGRATIONS_JWT_SECRET_KEY = SecretFileValue(None, environ_name="INTEGRATIONS_JWT_SECRET_KEY", environ_prefix=None)
|
||||
INTEGRATIONS_JWT_ALG = values.Value(
|
||||
"HS256",
|
||||
environ_name="INTEGRATIONS_JWT_ALG",
|
||||
environ_prefix=None,
|
||||
)
|
||||
INTEGRATIONS_JWT_ISSUER = values.Value(
|
||||
"lasuite-meet",
|
||||
environ_name="INTEGRATIONS_JWT_ISSUER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
INTEGRATIONS_JWT_EXPIRATION_SECONDS = values.PositiveIntegerValue(
|
||||
3600,
|
||||
environ_name="INTEGRATIONS_JWT_EXPIRATION_SECONDS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
INTEGRATIONS_JWT_TOKEN_TYPE = values.Value(
|
||||
"Bearer",
|
||||
environ_name="INTEGRATIONS_JWT_TOKEN_TYPE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
INTEGRATIONS_APP_BASE_URL = values.Value(
|
||||
None,
|
||||
environ_name="INTEGRATIONS_APP_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
@@ -73,6 +73,8 @@ backend:
|
||||
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
|
||||
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
||||
ROOM_SUBTITLE_ENABLED: True
|
||||
INTEGRATIONS_JWT_SECRET_KEY: {{ .Values.integrationJwtSecretKey }}
|
||||
INTEGRATIONS_APP_BASE_URL: https://meet.127.0.0.1.nip.io
|
||||
|
||||
|
||||
migrate:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
djangoSecretKey: 7K9mQ2xR8pL3vN6tY1sW4jH5cE0zF9bM2qA7uI3oP6rT1wErt12te12
|
||||
integrationJwtSecretKey: devKey
|
||||
livekit:
|
||||
keys:
|
||||
devkey: secret
|
||||
|
||||
@@ -74,6 +74,20 @@ spec:
|
||||
serviceName: {{ include "meet.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
- path: /external-api/
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "meet.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "meet.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
|
||||
Reference in New Issue
Block a user