Skip to content

Commit

Permalink
sources/plex: add property mappings (#10772)
Browse files Browse the repository at this point in the history
  • Loading branch information
rissson authored Aug 8, 2024
1 parent 82017fa commit 68af5b0
Show file tree
Hide file tree
Showing 14 changed files with 1,458 additions and 171 deletions.
31 changes: 31 additions & 0 deletions authentik/sources/plex/api/property_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Plex source property mappings API"""

from rest_framework.viewsets import ModelViewSet

from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.plex.models import PlexSourcePropertyMapping


class PlexSourcePropertyMappingSerializer(PropertyMappingSerializer):
"""PlexSourcePropertyMapping Serializer"""

class Meta(PropertyMappingSerializer.Meta):
model = PlexSourcePropertyMapping


class PlexSourcePropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for PlexSourcePropertyMapping"""

class Meta(PropertyMappingFilterSet.Meta):
model = PlexSourcePropertyMapping


class PlexSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""PlexSourcePropertyMapping Viewset"""

queryset = PlexSourcePropertyMapping.objects.all()
serializer_class = PlexSourcePropertyMappingSerializer
filterset_class = PlexSourcePropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]
11 changes: 8 additions & 3 deletions authentik/sources/plex/api/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response
from authentik.rbac.decorators import permission_required
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager

LOGGER = get_logger()
Expand All @@ -31,6 +31,7 @@ class PlexSourceSerializer(SourceSerializer):
class Meta:
model = PlexSource
fields = SourceSerializer.Meta.fields + [
"group_matching_mode",
"client_id",
"allowed_servers",
"allow_friends",
Expand Down Expand Up @@ -58,6 +59,7 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
"enrollment_flow",
"policy_engine_mode",
"user_matching_mode",
"group_matching_mode",
"client_id",
"allow_friends",
]
Expand Down Expand Up @@ -109,7 +111,10 @@ def redeem_token(self, request: Request) -> Response:
source=source,
request=request,
identifier=str(identifier),
user_info=user_info,
user_info={
"info": user_info,
"auth_api": auth_api,
},
policy_context={},
)
return to_stage_response(request, sfm.get_flow(plex_token=plex_token))
Expand Down Expand Up @@ -158,7 +163,7 @@ def redeem_token_authenticated(self, request: Request) -> Response:
friends_allowed = owner_api.check_friends_overlap(identifier)
servers_allowed = auth_api.check_server_overlap()
if any([friends_allowed, servers_allowed]):
PlexSourceConnection.objects.create(
UserPlexSourceConnection.objects.create(
plex_token=plex_token,
user=request.user,
identifier=identifier,
Expand Down
33 changes: 26 additions & 7 deletions authentik/sources/plex/api/source_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

from rest_framework.viewsets import ModelViewSet

from authentik.core.api.sources import UserSourceConnectionSerializer, UserSourceConnectionViewSet
from authentik.sources.plex.models import PlexSourceConnection
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
UserSourceConnectionSerializer,
UserSourceConnectionViewSet,
)
from authentik.sources.plex.models import GroupPlexSourceConnection, UserPlexSourceConnection


class PlexSourceConnectionSerializer(UserSourceConnectionSerializer):
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
"""Plex Source connection Serializer"""

class Meta(UserSourceConnectionSerializer.Meta):
model = PlexSourceConnection
model = UserPlexSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + [
"identifier",
"plex_token",
Expand All @@ -21,8 +26,22 @@ class Meta(UserSourceConnectionSerializer.Meta):
}


class PlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
"""Plex Source connection Serializer"""

queryset = PlexSourceConnection.objects.all()
serializer_class = PlexSourceConnectionSerializer
queryset = UserPlexSourceConnection.objects.all()
serializer_class = UserPlexSourceConnectionSerializer


class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""Plex Group-Source connection Serializer"""

class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupPlexSourceConnection


class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
"""Group-source connection Viewset"""

queryset = GroupPlexSourceConnection.objects.all()
serializer_class = GroupPlexSourceConnectionSerializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 5.0.7 on 2024-08-05 11:29

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
("authentik_sources_plex", "0003_alter_plexsource_plex_token"),
]

operations = [
migrations.CreateModel(
name="GroupPlexSourceConnection",
fields=[
(
"groupsourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.groupsourceconnection",
),
),
],
options={
"verbose_name": "Group Plex Source Connection",
"verbose_name_plural": "Group Plex Source Connections",
},
bases=("authentik_core.groupsourceconnection",),
),
migrations.CreateModel(
name="PlexSourcePropertyMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Plex Source Property Mapping",
"verbose_name_plural": "Plex Source Property Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.RenameModel(
old_name="PlexSourceConnection",
new_name="UserPlexSourceConnection",
),
]
67 changes: 62 additions & 5 deletions authentik/sources/plex/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Plex source"""

from typing import Any

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.http.request import HttpRequest
Expand All @@ -8,7 +10,12 @@
from rest_framework.fields import CharField
from rest_framework.serializers import BaseSerializer, Serializer

from authentik.core.models import Source, UserSourceConnection
from authentik.core.models import (
GroupSourceConnection,
PropertyMapping,
Source,
UserSourceConnection,
)
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.lib.generators import generate_id
Expand Down Expand Up @@ -60,6 +67,22 @@ def serializer(self) -> type[BaseSerializer]:

return PlexSourceSerializer

@property
def property_mapping_type(self) -> type[PropertyMapping]:
return PlexSourcePropertyMapping

def get_base_user_properties(self, info: dict[str, Any], **kwargs):
return {
"username": info.get("username"),
"email": info.get("email"),
"name": info.get("title"),
}

def get_base_group_properties(self, group_id: str, **kwargs):
return {
"name": group_id,
}

@property
def icon_url(self) -> str:
icon = super().icon_url
Expand Down Expand Up @@ -95,18 +118,52 @@ class Meta:
verbose_name_plural = _("Plex Sources")


class PlexSourceConnection(UserSourceConnection):
class PlexSourcePropertyMapping(PropertyMapping):
"""Map Plex properties to User of Group object attributes"""

@property
def component(self) -> str:
return "ak-property-mapping-plex-source-form"

@property
def serializer(self) -> type[Serializer]:
from authentik.sources.plex.api.property_mappings import PlexSourcePropertyMappingSerializer

return PlexSourcePropertyMappingSerializer

class Meta:
verbose_name = _("Plex Source Property Mapping")
verbose_name_plural = _("Plex Source Property Mappings")


class UserPlexSourceConnection(UserSourceConnection):
"""Connect user and plex source"""

plex_token = models.TextField()
identifier = models.TextField()

@property
def serializer(self) -> Serializer:
from authentik.sources.plex.api.source_connection import PlexSourceConnectionSerializer
def serializer(self) -> type[Serializer]:
from authentik.sources.plex.api.source_connection import UserPlexSourceConnectionSerializer

return PlexSourceConnectionSerializer
return UserPlexSourceConnectionSerializer

class Meta:
verbose_name = _("User Plex Source Connection")
verbose_name_plural = _("User Plex Source Connections")


class GroupPlexSourceConnection(GroupSourceConnection):
"""Group-source connection"""

@property
def serializer(self) -> type[Serializer]:
from authentik.sources.plex.api.source_connection import (
GroupPlexSourceConnectionSerializer,
)

return GroupPlexSourceConnectionSerializer

class Meta:
verbose_name = _("Group Plex Source Connection")
verbose_name_plural = _("Group Plex Source Connections")
14 changes: 5 additions & 9 deletions authentik/sources/plex/plex.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from authentik import __version__
from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.lib.utils.http import get_http_session
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection

LOGGER = get_logger()

Expand Down Expand Up @@ -73,11 +73,7 @@ def get_user_info(self) -> tuple[dict, int]:
)
response.raise_for_status()
raw_user_info = response.json()
return {
"username": raw_user_info.get("username"),
"email": raw_user_info.get("email"),
"name": raw_user_info.get("title"),
}, raw_user_info.get("id")
return raw_user_info, raw_user_info.get("id")

def check_server_overlap(self) -> bool:
"""Check if the plex-token has any server overlap with our configured servers"""
Expand Down Expand Up @@ -113,11 +109,11 @@ def check_friends_overlap(self, user_ident: int) -> bool:
class PlexSourceFlowManager(SourceFlowManager):
"""Flow manager for plex sources"""

user_connection_type = PlexSourceConnection
user_connection_type = UserPlexSourceConnection

def update_user_connection(
self, connection: PlexSourceConnection, **kwargs
) -> PlexSourceConnection:
self, connection: UserPlexSourceConnection, **kwargs
) -> UserPlexSourceConnection:
"""Set the access_token on the connection"""
connection.plex_token = kwargs.get("plex_token")
return connection
20 changes: 19 additions & 1 deletion authentik/sources/plex/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_get_user_info(self):
self.assertEqual(
api.get_user_info(),
(
{"username": "username", "email": "[email protected]", "name": "title"},
USER_INFO_RESPONSE,
1234123419,
),
)
Expand Down Expand Up @@ -82,3 +82,21 @@ def test_check_task(self):
mocker.get("https://plex.tv/api/v2/user", exc=RequestException())
check_plex_token_all()
self.assertTrue(Event.objects.filter(action=EventAction.CONFIGURATION_ERROR).exists())

def test_user_base_properties(self):
"""Test user base properties"""
properties = self.source.get_base_user_properties(info=USER_INFO_RESPONSE)
self.assertEqual(
properties,
{
"username": "username",
"name": "title",
"email": "[email protected]",
},
)

def test_group_base_properties(self):
"""Test group base properties"""
for group_id in ["group 1", "group 2"]:
properties = self.source.get_base_group_properties(group_id=group_id)
self.assertEqual(properties, {"name": group_id})
10 changes: 8 additions & 2 deletions authentik/sources/plex/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""API URLs"""

from authentik.sources.plex.api.property_mappings import PlexSourcePropertyMappingViewSet
from authentik.sources.plex.api.source import PlexSourceViewSet
from authentik.sources.plex.api.source_connection import PlexSourceConnectionViewSet
from authentik.sources.plex.api.source_connection import (
GroupPlexSourceConnectionViewSet,
UserPlexSourceConnectionViewSet,
)

api_urlpatterns = [
("sources/user_connections/plex", PlexSourceConnectionViewSet),
("propertymappings/source/plex", PlexSourcePropertyMappingViewSet),
("sources/user_connections/plex", UserPlexSourceConnectionViewSet),
("sources/group_connections/plex", GroupPlexSourceConnectionViewSet),
("sources/plex", PlexSourceViewSet),
]
Loading

0 comments on commit 68af5b0

Please sign in to comment.