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:
parent
d5cab5d580
commit
fa2ff5fc2b
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
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:
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Reference in New Issue