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.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)

View file

@ -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

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(
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:

View file

@ -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"""

View file

@ -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

View file

@ -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}