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:
Jens Langhammer 2021-05-05 19:24:10 +02:00
parent d5cab5d580
commit fa2ff5fc2b
7 changed files with 119 additions and 19 deletions

View File

@ -9,14 +9,16 @@ from rest_framework.permissions import AllowAny
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge 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.models import PlexSource
from authentik.sources.plex.plex import PlexAuth from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
LOGGER = get_logger()
class PlexSourceSerializer(SourceSerializer): class PlexSourceSerializer(SourceSerializer):
@ -24,7 +26,13 @@ class PlexSourceSerializer(SourceSerializer):
class Meta: class Meta:
model = PlexSource 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): class PlexTokenRedeemSerializer(PassiveSerializer):
@ -69,7 +77,29 @@ class PlexSourceViewSet(ModelViewSet):
if not plex_token: if not plex_token:
raise Http404 raise Http404
auth_api = PlexAuth(source, plex_token) 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 raise Http404
response = auth_api.get_user_url(request) sfm = PlexSourceFlowManager(
return to_stage_response(request, response) source=source,
request=request,
identifier=str(identifier),
enroll_info=user_info,
)
return sfm.get_flow(plex_token=plex_token)

View File

@ -3,6 +3,7 @@
import django.contrib.postgres.fields import django.contrib.postgres.fields
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import authentik.providers.oauth2.generators import authentik.providers.oauth2.generators

View 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,
),
),
]

View File

@ -29,6 +29,7 @@ class PlexSource(Source):
allowed_servers = ArrayField( allowed_servers = ArrayField(
models.TextField(), models.TextField(),
default=list, default=list,
blank=True,
help_text=_( help_text=_(
( (
"Which servers a user has to be a member of to be granted access. " "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 @property
def component(self) -> str: def component(self) -> str:

View File

@ -1,8 +1,7 @@
"""Plex Views""" """Plex Views"""
from urllib.parse import urlencode from urllib.parse import urlencode
from django.http.request import HttpRequest from django.http.response import Http404
from django.http.response import Http404, HttpResponse
from requests import Session from requests import Session
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -52,6 +51,18 @@ class PlexAuth:
response.raise_for_status() response.raise_for_status()
return response.json() 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]: def get_user_info(self) -> tuple[dict, int]:
"""Get user info of the plex token""" """Get user info of the plex token"""
qs = { qs = {
@ -87,17 +98,6 @@ class PlexAuth:
return True return True
return False 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): class PlexSourceFlowManager(SourceFlowManager):
"""Flow manager for plex sources""" """Flow manager for plex sources"""

View File

@ -18168,6 +18168,15 @@ definitions:
title: Allowed servers title: Allowed servers
type: string type: string
minLength: 1 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: PlexTokenRedeem:
required: required:
- plex_token - plex_token

View File

@ -20,6 +20,7 @@ export class PlexSourceForm extends Form<PlexSource> {
slug: value, slug: value,
}).then(source => { }).then(source => {
this.source = source; this.source = source;
this.plexToken = source.plexToken;
}); });
} }
@ -43,6 +44,7 @@ export class PlexSourceForm extends Form<PlexSource> {
} }
send = (data: PlexSource): Promise<PlexSource> => { send = (data: PlexSource): Promise<PlexSource> => {
data.plexToken = this.plexToken;
if (this.source.slug) { if (this.source.slug) {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({ return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
slug: this.source.slug, slug: this.source.slug,
@ -128,6 +130,14 @@ export class PlexSourceForm extends Form<PlexSource> {
name="clientId"> name="clientId">
<input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required> <input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required>
</ak-form-element-horizontal> </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 <ak-form-element-horizontal
label=${t`Allowed servers`} label=${t`Allowed servers`}
?required=${true} ?required=${true}