sources/plex: save user's plex token, add option to allow friends
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -9,14 +9,16 @@ from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.api.decorators import permission_required
 | 
			
		||||
from authentik.core.api.sources import SourceSerializer
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
from authentik.flows.challenge import RedirectChallenge
 | 
			
		||||
from authentik.flows.views import to_stage_response
 | 
			
		||||
from authentik.sources.plex.models import PlexSource
 | 
			
		||||
from authentik.sources.plex.plex import PlexAuth
 | 
			
		||||
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlexSourceSerializer(SourceSerializer):
 | 
			
		||||
@ -24,7 +26,13 @@ class PlexSourceSerializer(SourceSerializer):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PlexSource
 | 
			
		||||
        fields = SourceSerializer.Meta.fields + ["client_id", "allowed_servers"]
 | 
			
		||||
        fields = SourceSerializer.Meta.fields + [
 | 
			
		||||
            "client_id",
 | 
			
		||||
            "allowed_servers",
 | 
			
		||||
            "allow_friends",
 | 
			
		||||
            "plex_token",
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {"plex_token": {"write_only": True}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlexTokenRedeemSerializer(PassiveSerializer):
 | 
			
		||||
@ -69,7 +77,29 @@ class PlexSourceViewSet(ModelViewSet):
 | 
			
		||||
        if not plex_token:
 | 
			
		||||
            raise Http404
 | 
			
		||||
        auth_api = PlexAuth(source, plex_token)
 | 
			
		||||
        if not auth_api.check_server_overlap():
 | 
			
		||||
        user_info, identifier = auth_api.get_user_info()
 | 
			
		||||
        # Check friendship first, then check server overlay
 | 
			
		||||
        friends_allowed = False
 | 
			
		||||
        if source.allow_friends:
 | 
			
		||||
            owner_api = PlexAuth(source, source.plex_token)
 | 
			
		||||
            owner_friends = owner_api.get_friends()
 | 
			
		||||
            for friend in owner_friends:
 | 
			
		||||
                if int(friend.get("id", "0")) == int(identifier):
 | 
			
		||||
                    friends_allowed = True
 | 
			
		||||
                    LOGGER.info(
 | 
			
		||||
                        "allowing user for plex because of friend",
 | 
			
		||||
                        user=user_info["username"],
 | 
			
		||||
                    )
 | 
			
		||||
        if not auth_api.check_server_overlap() or not friends_allowed:
 | 
			
		||||
            LOGGER.warning(
 | 
			
		||||
                "Denying plex auth because no server overlay and no friends",
 | 
			
		||||
                user=user_info["username"],
 | 
			
		||||
            )
 | 
			
		||||
            raise Http404
 | 
			
		||||
        response = auth_api.get_user_url(request)
 | 
			
		||||
        return to_stage_response(request, response)
 | 
			
		||||
        sfm = PlexSourceFlowManager(
 | 
			
		||||
            source=source,
 | 
			
		||||
            request=request,
 | 
			
		||||
            identifier=str(identifier),
 | 
			
		||||
            enroll_info=user_info,
 | 
			
		||||
        )
 | 
			
		||||
        return sfm.get_flow(plex_token=plex_token)
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import authentik.providers.oauth2.generators
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								authentik/sources/plex/migrations/0002_auto_20210505_1717.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								authentik/sources/plex/migrations/0002_auto_20210505_1717.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-05 17:17
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import authentik.providers.oauth2.generators
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_sources_plex", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="plexsource",
 | 
			
		||||
            name="allow_friends",
 | 
			
		||||
            field=models.BooleanField(
 | 
			
		||||
                default=True,
 | 
			
		||||
                help_text="Allow friends to authenticate, even if you don't share a server.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="plexsource",
 | 
			
		||||
            name="plex_token",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                default="", help_text="Plex token used to check firends"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="plexsource",
 | 
			
		||||
            name="allowed_servers",
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(
 | 
			
		||||
                base_field=models.TextField(),
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=list,
 | 
			
		||||
                help_text="Which servers a user has to be a member of to be granted access. Empty list allows every server.",
 | 
			
		||||
                size=None,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -29,6 +29,7 @@ class PlexSource(Source):
 | 
			
		||||
    allowed_servers = ArrayField(
 | 
			
		||||
        models.TextField(),
 | 
			
		||||
        default=list,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "Which servers a user has to be a member of to be granted access. "
 | 
			
		||||
@ -36,6 +37,13 @@ class PlexSource(Source):
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    allow_friends = models.BooleanField(
 | 
			
		||||
        default=True,
 | 
			
		||||
        help_text=_("Allow friends to authenticate, even if you don't share a server."),
 | 
			
		||||
    )
 | 
			
		||||
    plex_token = models.TextField(
 | 
			
		||||
        default="", help_text=_("Plex token used to check firends")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
"""Plex Views"""
 | 
			
		||||
from urllib.parse import urlencode
 | 
			
		||||
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.http.response import Http404, HttpResponse
 | 
			
		||||
from django.http.response import Http404
 | 
			
		||||
from requests import Session
 | 
			
		||||
from requests.exceptions import RequestException
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
@ -52,6 +51,18 @@ class PlexAuth:
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return response.json()
 | 
			
		||||
 | 
			
		||||
    def get_friends(self) -> list[dict]:
 | 
			
		||||
        """Get plex friends"""
 | 
			
		||||
        qs = {
 | 
			
		||||
            "X-Plex-Token": self._token,
 | 
			
		||||
            "X-Plex-Client-Identifier": self._source.client_id,
 | 
			
		||||
        }
 | 
			
		||||
        response = self._session.get(
 | 
			
		||||
            f"https://plex.tv/api/v2/friends?{urlencode(qs)}",
 | 
			
		||||
        )
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return response.json()
 | 
			
		||||
 | 
			
		||||
    def get_user_info(self) -> tuple[dict, int]:
 | 
			
		||||
        """Get user info of the plex token"""
 | 
			
		||||
        qs = {
 | 
			
		||||
@ -87,17 +98,6 @@ class PlexAuth:
 | 
			
		||||
                    return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_user_url(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        """Get a URL to a flow executor for either enrollment or authentication"""
 | 
			
		||||
        user_info, identifier = self.get_user_info()
 | 
			
		||||
        sfm = PlexSourceFlowManager(
 | 
			
		||||
            source=self._source,
 | 
			
		||||
            request=request,
 | 
			
		||||
            identifier=str(identifier),
 | 
			
		||||
            enroll_info=user_info,
 | 
			
		||||
        )
 | 
			
		||||
        return sfm.get_flow(plex_token=self._token)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlexSourceFlowManager(SourceFlowManager):
 | 
			
		||||
    """Flow manager for plex sources"""
 | 
			
		||||
 | 
			
		||||
@ -18168,6 +18168,15 @@ definitions:
 | 
			
		||||
          title: Allowed servers
 | 
			
		||||
          type: string
 | 
			
		||||
          minLength: 1
 | 
			
		||||
      allow_friends:
 | 
			
		||||
        title: Allow friends
 | 
			
		||||
        description: Allow friends to authenticate, even if you don't share a server.
 | 
			
		||||
        type: boolean
 | 
			
		||||
      plex_token:
 | 
			
		||||
        title: Plex token
 | 
			
		||||
        description: Plex token used to check firends
 | 
			
		||||
        type: string
 | 
			
		||||
        minLength: 1
 | 
			
		||||
  PlexTokenRedeem:
 | 
			
		||||
    required:
 | 
			
		||||
      - plex_token
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ export class PlexSourceForm extends Form<PlexSource> {
 | 
			
		||||
            slug: value,
 | 
			
		||||
        }).then(source => {
 | 
			
		||||
            this.source = source;
 | 
			
		||||
            this.plexToken = source.plexToken;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -43,6 +44,7 @@ export class PlexSourceForm extends Form<PlexSource> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    send = (data: PlexSource): Promise<PlexSource> => {
 | 
			
		||||
        data.plexToken = this.plexToken;
 | 
			
		||||
        if (this.source.slug) {
 | 
			
		||||
            return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
 | 
			
		||||
                slug: this.source.slug,
 | 
			
		||||
@ -128,6 +130,14 @@ export class PlexSourceForm extends Form<PlexSource> {
 | 
			
		||||
                        name="clientId">
 | 
			
		||||
                        <input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal name="allowFriends">
 | 
			
		||||
                        <div class="pf-c-check">
 | 
			
		||||
                            <input type="checkbox" class="pf-c-check__input" ?checked=${first(this.source?.allowFriends, true)}>
 | 
			
		||||
                            <label class="pf-c-check__label">
 | 
			
		||||
                                ${t`Allow friends to authenticate via Plex, even if you don't share any servers`}
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${t`Allowed servers`}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user