Merge branch 'master' into outpost-ldap
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> # Conflicts: # authentik/core/api/users.py # authentik/policies/event_matcher/migrations/0013_alter_eventmatcherpolicy_app.py
This commit is contained in:
commit
99d161e212
|
@ -33,6 +33,8 @@ values =
|
|||
|
||||
[bumpversion:file:authentik/__init__.py]
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:outpost/pkg/version.go]
|
||||
|
||||
[bumpversion:file:web/src/constants.ts]
|
||||
|
|
30
Dockerfile
30
Dockerfile
|
@ -1,3 +1,4 @@
|
|||
# Stage 1: Lock python dependencies
|
||||
FROM python:3.9-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
|
@ -9,6 +10,34 @@ RUN pip install pipenv && \
|
|||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
# Stage 2: Build webui
|
||||
FROM node as npm-builder
|
||||
|
||||
COPY ./web /static/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i --production=false && npm run build
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM golang:1.16.3 AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY --from=npm-builder /static/robots.txt /work/web/robots.txt
|
||||
COPY --from=npm-builder /static/security.txt /work/web/security.txt
|
||||
COPY --from=npm-builder /static/dist/ /work/web/dist/
|
||||
COPY --from=npm-builder /static/authentik/ /work/web/authentik/
|
||||
|
||||
# RUN ls /work/web/static/authentik/ && exit 1
|
||||
COPY ./cmd /work/cmd
|
||||
COPY ./web/static.go /work/web/static.go
|
||||
COPY ./internal /work/internal
|
||||
COPY ./go.mod /work/go.mod
|
||||
COPY ./go.sum /work/go.sum
|
||||
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 4: Run
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
|
@ -44,6 +73,7 @@ COPY ./pyproject.toml /
|
|||
COPY ./xml /xml
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY --from=builder /work/authentik /authentik-proxy
|
||||
|
||||
USER authentik
|
||||
STOPSIGNAL SIGINT
|
||||
|
|
12
Makefile
12
Makefile
|
@ -1,4 +1,4 @@
|
|||
all: lint-fix lint coverage gen
|
||||
all: lint-fix lint test gen
|
||||
|
||||
test-integration:
|
||||
k3d cluster create || exit 0
|
||||
|
@ -8,7 +8,7 @@ test-integration:
|
|||
test-e2e:
|
||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||
|
||||
coverage:
|
||||
test:
|
||||
coverage run manage.py test -v 3 authentik
|
||||
coverage html
|
||||
coverage report
|
||||
|
@ -22,7 +22,7 @@ lint:
|
|||
bandit -r authentik tests lifecycle -x node_modules
|
||||
pylint authentik tests lifecycle
|
||||
|
||||
gen: coverage
|
||||
gen:
|
||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||
|
||||
local-stack:
|
||||
|
@ -31,7 +31,5 @@ local-stack:
|
|||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
||||
|
||||
build-static:
|
||||
docker-compose -f scripts/ci.docker-compose.yml up -d
|
||||
docker build -t beryju/authentik-static -f static.Dockerfile --network=scripts_default .
|
||||
docker-compose -f scripts/ci.docker-compose.yml down -v
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
|
|
1
Pipfile
1
Pipfile
|
@ -59,3 +59,4 @@ pylint-django = "*"
|
|||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
selenium = "*"
|
||||
requests-mock = "*"
|
||||
|
|
76
Pipfile.lock
generated
76
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "24f00363590649f2442c6ac28dfe8692f0f317e0a5b91c0696b84610cef299d2"
|
||||
"sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -116,18 +116,18 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:35b099fa55f5db6e99a92855b9f320736121ae985340adfc73bc46fb443809e9",
|
||||
"sha256:53fd4c7df86f78e51168f832b42ca1c284333b3f5af0266bf10d13af41aeff5c"
|
||||
"sha256:ac10d832ad716281da6ca77cea824d723af479f8611087dee4b0489c48c32fd9",
|
||||
"sha256:e2ef25afc36a301199bfbd662aef46dd11ed0db9baf96fce111db4043928065b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.61"
|
||||
"version": "==1.17.64"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:c765ddd0648e32b375ced8b82bfcc3f8437107278b2d2c73b7da7f41297b5388",
|
||||
"sha256:d48f94573c75a6c1d6d0152b9e21432083a1b0a0fc39b41f57128464982cb0a0"
|
||||
"sha256:42dde7c699b3710e5c3a944cd8ce8b7a80b9f610d8857a0ad36bdc9743cc3375",
|
||||
"sha256:ec418c273c37efd33d39bb4559f7df09de46df1f87fdbb064d8ebb281849a625"
|
||||
],
|
||||
"version": "==1.20.61"
|
||||
"version": "==1.20.64"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
|
@ -607,18 +607,24 @@
|
|||
"sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d",
|
||||
"sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
|
||||
"sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
|
||||
"sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae",
|
||||
"sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
|
||||
"sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
|
||||
"sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
|
||||
"sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
|
||||
"sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59",
|
||||
"sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
|
||||
"sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
|
||||
"sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96",
|
||||
"sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
|
||||
"sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
|
||||
"sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
|
||||
"sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354",
|
||||
"sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
|
||||
"sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
|
||||
"sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16",
|
||||
"sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
|
||||
"sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a",
|
||||
"sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
|
||||
"sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
|
||||
"sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a",
|
||||
|
@ -631,10 +637,14 @@
|
|||
"sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
|
||||
"sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
|
||||
"sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
|
||||
"sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617",
|
||||
"sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
|
||||
"sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92",
|
||||
"sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
|
||||
"sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
|
||||
"sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24",
|
||||
"sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
|
||||
"sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e",
|
||||
"sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
|
||||
"sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
|
||||
"sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
|
||||
|
@ -1168,11 +1178,11 @@
|
|||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
||||
],
|
||||
"version": "==3.7.4.3"
|
||||
"version": "==3.10.0.0"
|
||||
},
|
||||
"uritemplate": {
|
||||
"hashes": [
|
||||
|
@ -1442,6 +1452,20 @@
|
|||
"index": "pypi",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
],
|
||||
"version": "==2020.12.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
|
@ -1529,6 +1553,13 @@
|
|||
],
|
||||
"version": "==3.1.15"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
|
@ -1747,6 +1778,21 @@
|
|||
],
|
||||
"version": "==2021.4.4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-mock": {
|
||||
"hashes": [
|
||||
"sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595",
|
||||
"sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.2"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
|
||||
|
@ -1820,11 +1866,11 @@
|
|||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
||||
],
|
||||
"version": "==3.7.4.3"
|
||||
"version": "==3.10.0.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
authentik API Browser
|
||||
API Browser - {{ config.authentik.branding.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
|
|
@ -64,6 +64,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
|||
from authentik.sources.oauth.api.source_connection import (
|
||||
UserOAuthSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.plex.api import PlexSourceViewSet
|
||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||
from authentik.stages.authenticator_static.api import (
|
||||
AuthenticatorStaticStageViewSet,
|
||||
|
@ -138,6 +139,7 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
|
|||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/saml", SAMLSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
router.register("sources/plex", PlexSourceViewSet)
|
||||
|
||||
router.register("policies/all", PolicyViewSet)
|
||||
router.register("policies/bindings", PolicyBindingViewSet)
|
||||
|
|
|
@ -45,6 +45,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"policy_engine_mode",
|
||||
"user_matching_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
"""User API Views"""
|
||||
from json import loads
|
||||
|
||||
from django.http.response import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django_filters.filters import CharFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import BooleanField, ListSerializer, ModelSerializer
|
||||
from rest_framework.serializers import (
|
||||
BooleanField,
|
||||
ListSerializer,
|
||||
ModelSerializer,
|
||||
ValidationError,
|
||||
)
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
|
@ -87,13 +96,42 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||
)
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
"""Filter for users"""
|
||||
|
||||
attributes = CharFilter(
|
||||
field_name="attributes",
|
||||
lookup_expr="",
|
||||
label="Attributes",
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
"""Filter attributes by query args"""
|
||||
try:
|
||||
value = loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(detail="filter: failed to parse JSON")
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(detail="filter: value must be key:value mapping")
|
||||
qs = {}
|
||||
for key, _value in value.items():
|
||||
qs[f"attributes__{key}"] = _value
|
||||
return queryset.filter(**qs)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "name", "is_active", "attributes"]
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
serializer_class = UserSerializer
|
||||
search_fields = ["username", "name", "is_active"]
|
||||
filterset_fields = ["username", "name", "is_active"]
|
||||
filterset_class = UsersFilter
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||
|
|
40
authentik/core/migrations/0020_source_user_matching_mode.py
Normal file
40
authentik/core/migrations/0020_source_user_matching_mode.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.2 on 2021-05-03 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_source_managed"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="user_matching_mode",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("identifier", "Use the source-specific identifier"),
|
||||
(
|
||||
"email_link",
|
||||
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
|
||||
),
|
||||
(
|
||||
"email_deny",
|
||||
"Use the user's email address, but deny enrollment when the email address already exists.",
|
||||
),
|
||||
(
|
||||
"username_link",
|
||||
"Link to a user with identical username address. Can have security implications when a username is used with another source.",
|
||||
),
|
||||
(
|
||||
"username_deny",
|
||||
"Use the user's username, but deny enrollment when the username already exists.",
|
||||
),
|
||||
],
|
||||
default="identifier",
|
||||
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -240,6 +240,30 @@ class Application(PolicyBindingModel):
|
|||
verbose_name_plural = _("Applications")
|
||||
|
||||
|
||||
class SourceUserMatchingModes(models.TextChoices):
|
||||
"""Different modes a source can handle new/returning users"""
|
||||
|
||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||
EMAIL_LINK = "email_link", _(
|
||||
(
|
||||
"Link to a user with identical email address. Can have security implications "
|
||||
"when a source doesn't validate email addresses."
|
||||
)
|
||||
)
|
||||
EMAIL_DENY = "email_deny", _(
|
||||
"Use the user's email address, but deny enrollment when the email address already exists."
|
||||
)
|
||||
USERNAME_LINK = "username_link", _(
|
||||
(
|
||||
"Link to a user with identical username address. Can have security implications "
|
||||
"when a username is used with another source."
|
||||
)
|
||||
)
|
||||
USERNAME_DENY = "username_deny", _(
|
||||
"Use the user's username, but deny enrollment when the username already exists."
|
||||
)
|
||||
|
||||
|
||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
|
@ -272,6 +296,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||
related_name="source_enrollment",
|
||||
)
|
||||
|
||||
user_matching_mode = models.TextField(
|
||||
choices=SourceUserMatchingModes.choices,
|
||||
default=SourceUserMatchingModes.IDENTIFIER,
|
||||
help_text=_(
|
||||
(
|
||||
"How the source determines if an existing user should be authenticated or "
|
||||
"a new user enrolled."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
|
@ -301,6 +336,8 @@ class UserSourceConnection(CreatedUpdatedModel):
|
|||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
class Meta:
|
||||
|
||||
unique_together = (("user", "source"),)
|
||||
|
|
0
authentik/core/sources/__init__.py
Normal file
0
authentik/core/sources/__init__.py
Normal file
276
authentik/core/sources/flow_manager.py
Normal file
276
authentik/core/sources/flow_manager.py
Normal file
|
@ -0,0 +1,276 @@
|
|||
"""Source decision helper"""
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db.models.query_utils import Q
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import (
|
||||
Source,
|
||||
SourceUserMatchingModes,
|
||||
User,
|
||||
UserSourceConnection,
|
||||
)
|
||||
from authentik.core.sources.stage import (
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION,
|
||||
PostUserEnrollmentStage,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow, Stage, in_memory_stage
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""Actions that can be decided based on the request
|
||||
and source settings"""
|
||||
|
||||
LINK = "link"
|
||||
AUTH = "auth"
|
||||
ENROLL = "enroll"
|
||||
DENY = "deny"
|
||||
|
||||
|
||||
class SourceFlowManager:
|
||||
"""Help sources decide what they should do after authorization. Based on source settings and
|
||||
previous connections, authenticate the user, enroll a new user, link to an existing user
|
||||
or deny the request."""
|
||||
|
||||
source: Source
|
||||
request: HttpRequest
|
||||
|
||||
identifier: str
|
||||
|
||||
connection_type: Type[UserSourceConnection] = UserSourceConnection
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: Source,
|
||||
request: HttpRequest,
|
||||
identifier: str,
|
||||
enroll_info: dict[str, Any],
|
||||
) -> None:
|
||||
self.source = source
|
||||
self.request = request
|
||||
self.identifier = identifier
|
||||
self.enroll_info = enroll_info
|
||||
self._logger = get_logger().bind(source=source, identifier=identifier)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]:
|
||||
"""decide which action should be taken"""
|
||||
new_connection = self.connection_type(
|
||||
source=self.source, identifier=self.identifier
|
||||
)
|
||||
# When request is authenticated, always link
|
||||
if self.request.user.is_authenticated:
|
||||
new_connection.user = self.request.user
|
||||
new_connection = self.update_connection(new_connection, **kwargs)
|
||||
new_connection.save()
|
||||
return Action.LINK, new_connection
|
||||
|
||||
existing_connections = self.connection_type.objects.filter(
|
||||
source=self.source, identifier=self.identifier
|
||||
)
|
||||
if existing_connections.exists():
|
||||
connection = existing_connections.first()
|
||||
return Action.AUTH, self.update_connection(connection, **kwargs)
|
||||
# No connection exists, but we match on identifier, so enroll
|
||||
if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER:
|
||||
# We don't save the connection here cause it doesn't have a user assigned yet
|
||||
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
|
||||
|
||||
# Check for existing users with matching attributes
|
||||
query = Q()
|
||||
# Either query existing user based on email or username
|
||||
if self.source.user_matching_mode in [
|
||||
SourceUserMatchingModes.EMAIL_LINK,
|
||||
SourceUserMatchingModes.EMAIL_DENY,
|
||||
]:
|
||||
if not self.enroll_info.get("email", None):
|
||||
self._logger.warning("Refusing to use none email", source=self.source)
|
||||
return Action.DENY, None
|
||||
query = Q(email__exact=self.enroll_info.get("email", None))
|
||||
if self.source.user_matching_mode in [
|
||||
SourceUserMatchingModes.USERNAME_LINK,
|
||||
SourceUserMatchingModes.USERNAME_DENY,
|
||||
]:
|
||||
if not self.enroll_info.get("username", None):
|
||||
self._logger.warning(
|
||||
"Refusing to use none username", source=self.source
|
||||
)
|
||||
return Action.DENY, None
|
||||
query = Q(username__exact=self.enroll_info.get("username", None))
|
||||
matching_users = User.objects.filter(query)
|
||||
# No matching users, always enroll
|
||||
if not matching_users.exists():
|
||||
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
|
||||
|
||||
user = matching_users.first()
|
||||
if self.source.user_matching_mode in [
|
||||
SourceUserMatchingModes.EMAIL_LINK,
|
||||
SourceUserMatchingModes.USERNAME_LINK,
|
||||
]:
|
||||
new_connection.user = user
|
||||
new_connection = self.update_connection(new_connection, **kwargs)
|
||||
new_connection.save()
|
||||
return Action.LINK, new_connection
|
||||
if self.source.user_matching_mode in [
|
||||
SourceUserMatchingModes.EMAIL_DENY,
|
||||
SourceUserMatchingModes.USERNAME_DENY,
|
||||
]:
|
||||
self._logger.info("denying source because user exists", user=user)
|
||||
return Action.DENY, None
|
||||
# Should never get here as default enroll case is returned above.
|
||||
return Action.DENY, None
|
||||
|
||||
def update_connection(
|
||||
self, connection: UserSourceConnection, **kwargs
|
||||
) -> UserSourceConnection:
|
||||
"""Optionally make changes to the connection after it is looked up/created."""
|
||||
return connection
|
||||
|
||||
def get_flow(self, **kwargs) -> HttpResponse:
|
||||
"""Get the flow response based on user_matching_mode"""
|
||||
action, connection = self.get_action()
|
||||
if not connection:
|
||||
return redirect("/")
|
||||
if action == Action.LINK:
|
||||
self._logger.debug("Linking existing user")
|
||||
return self.handle_existing_user_link(connection)
|
||||
if action == Action.AUTH:
|
||||
self._logger.debug("Handling auth user")
|
||||
return self.handle_auth_user(connection)
|
||||
if action == Action.ENROLL:
|
||||
self._logger.debug("Handling enrollment of new user")
|
||||
return self.handle_enroll(connection)
|
||||
# Default case, assume deny
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
"Request to authenticate with %(source)s has been denied!"
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
return redirect("/")
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||
"""Hook to override stages which are appended to the flow"""
|
||||
if flow.slug == self.source.enrollment_flow.slug:
|
||||
return [
|
||||
in_memory_stage(PostUserEnrollmentStage),
|
||||
]
|
||||
return []
|
||||
|
||||
def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-admin"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
}
|
||||
)
|
||||
if not flow:
|
||||
return HttpResponseBadRequest()
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_auth_user(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Login user and redirect."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs)
|
||||
|
||||
def handle_existing_user_link(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Handler when the user was already authenticated and linked an external source
|
||||
to their account."""
|
||||
# Connection has already been saved
|
||||
Event.new(
|
||||
EventAction.SOURCE_LINKED,
|
||||
message="Linked Source",
|
||||
source=self.source,
|
||||
).from_http(self.request)
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth_user(connection)
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_core:if-admin",
|
||||
)
|
||||
+ f"#/user;page-{self.source.slug}"
|
||||
)
|
||||
|
||||
def handle_enroll(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""User was not authenticated and previous request was not authenticated."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
if not self.source.enrollment_flow:
|
||||
self._logger.warning("source has no enrollment flow")
|
||||
return HttpResponseBadRequest()
|
||||
return self._handle_login_flow(
|
||||
self.source.enrollment_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
|
||||
},
|
||||
)
|
|
@ -1,32 +1,30 @@
|
|||
"""OAuth Stages"""
|
||||
"""Source flow manager stages"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import User, UserSourceConnection
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.sources.oauth.models import UserOAuthSourceConnection
|
||||
|
||||
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS = "sources_oauth_access"
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
|
||||
|
||||
|
||||
class PostUserEnrollmentStage(StageView):
|
||||
"""Dynamically injected stage which saves the OAuth Connection after
|
||||
"""Dynamically injected stage which saves the Connection after
|
||||
the user has been enrolled."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Stage used after the user has been enrolled"""
|
||||
access: UserOAuthSourceConnection = self.executor.plan.context[
|
||||
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS
|
||||
connection: UserSourceConnection = self.executor.plan.context[
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION
|
||||
]
|
||||
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
connection.user = user
|
||||
connection.save()
|
||||
Event.new(
|
||||
EventAction.SOURCE_LINKED,
|
||||
message="Linked OAuth Source",
|
||||
source=access.source,
|
||||
message="Linked Source",
|
||||
source=connection.source,
|
||||
).from_http(self.request)
|
||||
return self.executor.stage_ok()
|
|
@ -1,11 +1,14 @@
|
|||
"""authentik core models tests"""
|
||||
from time import sleep
|
||||
from typing import Callable, Type
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Token
|
||||
from authentik.core.models import Provider, Source, Token
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class TestModels(TestCase):
|
||||
|
@ -24,3 +27,40 @@ class TestModels(TestCase):
|
|||
)
|
||||
sleep(0.5)
|
||||
self.assertFalse(token.is_expired)
|
||||
|
||||
|
||||
def source_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
"""Test source"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
model_class.slug = "test"
|
||||
self.assertIsNotNone(model_class.component)
|
||||
_ = model_class.ui_login_button
|
||||
_ = model_class.ui_user_settings
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
"""Test provider"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
self.assertIsNotNone(model_class.component)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for model in all_subclasses(Source):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
|
||||
for model in all_subclasses(Provider):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import CharField, DictField
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.flows.challenge import Challenge
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -14,8 +15,8 @@ class UILoginButton:
|
|||
# Name, ran through i18n
|
||||
name: str
|
||||
|
||||
# URL Which Button points to
|
||||
url: str
|
||||
# Challenge which is presented to the user when they click the button
|
||||
challenge: Challenge
|
||||
|
||||
# Icon URL, used as-is
|
||||
icon_url: Optional[str] = None
|
||||
|
@ -25,7 +26,7 @@ class UILoginButtonSerializer(PassiveSerializer):
|
|||
"""Serializer for Login buttons of sources"""
|
||||
|
||||
name = CharField()
|
||||
url = CharField()
|
||||
challenge = DictField()
|
||||
icon_url = CharField(required=False, allow_null=True)
|
||||
|
||||
|
||||
|
|
|
@ -35,7 +35,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
|||
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
||||
return
|
||||
event: Event = events.first()
|
||||
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
|
||||
triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name)
|
||||
if not triggers.exists():
|
||||
return
|
||||
trigger = triggers.first()
|
||||
|
||||
if "policy_uuid" in event.context:
|
||||
policy_uuid = event.context["policy_uuid"]
|
||||
|
|
|
@ -16,17 +16,14 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
|
|||
"""Test a form"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
try:
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
self.assertTrue(issubclass(model_class.type, StageView))
|
||||
self.assertIsNotNone(test_model.component)
|
||||
_ = test_model.ui_user_settings
|
||||
except NotImplementedError:
|
||||
pass
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
self.assertTrue(issubclass(model_class.type, StageView))
|
||||
self.assertIsNotNone(test_model.component)
|
||||
_ = test_model.ui_user_settings
|
||||
|
||||
return tester
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ class ConfigLoader:
|
|||
# Walk each component of the path
|
||||
path_parts = path.split(sep)
|
||||
for comp in path_parts[:-1]:
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if comp not in root:
|
||||
root[comp] = {}
|
||||
root = root.get(comp)
|
||||
|
|
|
@ -5,6 +5,10 @@ postgresql:
|
|||
user: authentik
|
||||
password: 'env://POSTGRES_PASSWORD'
|
||||
|
||||
web:
|
||||
listen: 0.0.0.0:9000
|
||||
listen_tls: 0.0.0.0:9443
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
password: ''
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2 on 2021-04-26 09:27
|
||||
# Generated by Django 3.2 on 2021-05-02 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -35,12 +35,12 @@ class Migration(migrations.Migration):
|
|||
("authentik.policies.password", "authentik Policies.Password"),
|
||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
||||
("authentik.providers.ldap", "authentik Providers.LDAP"),
|
||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
||||
("authentik.recovery", "authentik Recovery"),
|
||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
||||
("authentik.sources.plex", "authentik Sources.Plex"),
|
||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
||||
(
|
||||
"authentik.stages.authenticator_static",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Permission denied - authentik' %}
|
||||
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'End session' %}
|
||||
{% trans 'End session' %} - {{ config.authentik.branding.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
|
|
|
@ -108,6 +108,7 @@ INSTALLED_APPS = [
|
|||
"authentik.recovery",
|
||||
"authentik.sources.ldap",
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.stages.authenticator_static",
|
||||
"authentik.stages.authenticator_totp",
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""authentik URL Configuration"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import include, path
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
|
@ -49,11 +47,3 @@ urlpatterns += [
|
|||
path("-/health/live/", LiveView.as_view(), name="health-live"),
|
||||
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
|
||||
]
|
||||
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
|
||||
urlpatterns = (
|
||||
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
+ urlpatterns
|
||||
)
|
||||
|
|
|
@ -2,11 +2,21 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.facebook",
|
||||
"authentik.sources.oauth.types.github",
|
||||
"authentik.sources.oauth.types.google",
|
||||
"authentik.sources.oauth.types.reddit",
|
||||
"authentik.sources.oauth.types.twitter",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.oidc",
|
||||
]
|
||||
|
||||
|
||||
class AuthentikSourceOAuthConfig(AppConfig):
|
||||
"""authentik source.oauth config"""
|
||||
|
@ -18,7 +28,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
"""Load source_types from config file"""
|
||||
for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES:
|
||||
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
|
||||
try:
|
||||
import_module(source_type)
|
||||
LOGGER.debug("Loaded OAuth Source Type", type=source_type)
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
"""authentik oauth_client Authorization backend"""
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http import HttpRequest
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
|
||||
class AuthorizedServiceBackend(ModelBackend):
|
||||
"Authentication backend for users registered with remote OAuth provider."
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, source: OAuthSource, identifier: str
|
||||
) -> Optional[User]:
|
||||
"Fetch user for a given source by id."
|
||||
access = UserOAuthSourceConnection.objects.filter(
|
||||
source=source, identifier=identifier
|
||||
).select_related("user")
|
||||
if not access.exists():
|
||||
return None
|
||||
return access.first().user
|
|
@ -9,6 +9,7 @@ from rest_framework.serializers import Serializer
|
|||
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.sources.oauth.types.manager import SourceType
|
||||
|
@ -67,9 +68,14 @@ class OAuthSource(Source):
|
|||
@property
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
url=reverse(
|
||||
"authentik_sources_oauth:oauth-client-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
challenge=RedirectChallenge(
|
||||
instance={
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": reverse(
|
||||
"authentik_sources_oauth:oauth-client-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
}
|
||||
),
|
||||
icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
|
||||
name=self.name,
|
||||
|
@ -163,16 +169,6 @@ class OpenIDOAuthSource(OAuthSource):
|
|||
verbose_name_plural = _("OpenID OAuth Sources")
|
||||
|
||||
|
||||
class PlexOAuthSource(OAuthSource):
|
||||
"""Login using plex.tv."""
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _("Plex OAuth Source")
|
||||
verbose_name_plural = _("Plex OAuth Sources")
|
||||
|
||||
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
"""Oauth2 Client Settings"""
|
||||
|
||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.facebook",
|
||||
"authentik.sources.oauth.types.github",
|
||||
"authentik.sources.oauth.types.google",
|
||||
"authentik.sources.oauth.types.reddit",
|
||||
"authentik.sources.oauth.types.twitter",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.oidc",
|
||||
"authentik.sources.oauth.types.plex",
|
||||
]
|
|
@ -1,7 +1,7 @@
|
|||
"""Discord Type tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
|
||||
|
||||
# https://discord.com/developers/docs/resources/user#user-object
|
||||
|
@ -33,9 +33,7 @@ class TestTypeDiscord(TestCase):
|
|||
|
||||
def test_enroll_context(self):
|
||||
"""Test discord Enrollment context"""
|
||||
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), DISCORD_USER
|
||||
)
|
||||
ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER)
|
||||
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
|
||||
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
|
||||
self.assertEqual(ak_context["name"], DISCORD_USER["username"])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""GitHub Type tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
|
||||
|
||||
# https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||
|
@ -63,9 +63,7 @@ class TestTypeGitHub(TestCase):
|
|||
|
||||
def test_enroll_context(self):
|
||||
"""Test GitHub Enrollment context"""
|
||||
ak_context = GitHubOAuth2Callback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), GITHUB_USER
|
||||
)
|
||||
ak_context = GitHubOAuth2Callback().get_user_enroll_context(GITHUB_USER)
|
||||
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
|
||||
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
|
||||
self.assertEqual(ak_context["name"], GITHUB_USER["name"])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""google Type tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.google import GoogleOAuth2Callback
|
||||
|
||||
# https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en
|
||||
|
@ -32,9 +32,7 @@ class TestTypeGoogle(TestCase):
|
|||
|
||||
def test_enroll_context(self):
|
||||
"""Test Google Enrollment context"""
|
||||
ak_context = GoogleOAuth2Callback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), GOOGLE_USER
|
||||
)
|
||||
ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER)
|
||||
self.assertEqual(ak_context["username"], GOOGLE_USER["email"])
|
||||
self.assertEqual(ak_context["email"], GOOGLE_USER["email"])
|
||||
self.assertEqual(ak_context["name"], GOOGLE_USER["name"])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Twitter Type tests"""
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
||||
|
||||
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
|
||||
|
@ -104,9 +104,7 @@ class TestTypeGitHub(TestCase):
|
|||
|
||||
def test_enroll_context(self):
|
||||
"""Test Twitter Enrollment context"""
|
||||
ak_context = TwitterOAuthCallback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), TWITTER_USER
|
||||
)
|
||||
ak_context = TwitterOAuthCallback().get_user_enroll_context(TWITTER_USER)
|
||||
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
|
||||
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
|
||||
self.assertEqual(ak_context["name"], TWITTER_USER["name"])
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
|
||||
|
@ -10,7 +9,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback
|
|||
class AzureADOAuthCallback(OAuthCallback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
def get_user_id(self, source: OAuthSource, info: dict[str, Any]) -> Optional[str]:
|
||||
def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
|
||||
try:
|
||||
return str(UUID(info.get("objectId")).int)
|
||||
except TypeError:
|
||||
|
@ -18,8 +17,6 @@ class AzureADOAuthCallback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Discord OAuth Views"""
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
@ -21,8 +20,6 @@ class DiscordOAuth2Callback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,6 @@ from typing import Any, Optional
|
|||
from facebook import GraphAPI
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
@ -34,8 +33,6 @@ class FacebookOAuth2Callback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""GitHub OAuth Views"""
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
|
||||
|
@ -11,8 +10,6 @@ class GitHubOAuth2Callback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Google OAuth Views"""
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
@ -21,8 +20,6 @@ class GoogleOAuth2Callback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""OpenID Connect OAuth Views"""
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
@ -19,13 +19,11 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
|
|||
class OpenIDConnectOAuth2Callback(OAuthCallback):
|
||||
"""OpenIDConnect OAuth2 Callback"""
|
||||
|
||||
def get_user_id(self, source: OAuthSource, info: dict[str, str]) -> str:
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
"""Plex OAuth Views"""
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.http.response import Http404
|
||||
from requests import post
|
||||
from requests.api import get
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_ID_KEY = "PLEX_ID"
|
||||
SESSION_CODE_KEY = "PLEX_CODE"
|
||||
DEFAULT_PAYLOAD = {
|
||||
"X-Plex-Product": "authentik",
|
||||
"X-Plex-Version": __version__,
|
||||
"X-Plex-Device-Vendor": "BeryJu.org",
|
||||
}
|
||||
|
||||
|
||||
class PlexRedirect(OAuthRedirect):
|
||||
"""Plex Auth redirect, get a pin then redirect to a URL to claim it"""
|
||||
|
||||
headers = {}
|
||||
|
||||
def get_pin(self, **data) -> dict:
|
||||
"""Get plex pin that the user will claim
|
||||
https://forums.plex.tv/t/authenticating-with-plex/609370"""
|
||||
return post(
|
||||
"https://plex.tv/api/v2/pins.json?strong=true",
|
||||
data=data,
|
||||
headers=self.headers,
|
||||
).json()
|
||||
|
||||
def get_redirect_url(self, **kwargs) -> str:
|
||||
slug = kwargs.get("source_slug", "")
|
||||
self.headers = {"Origin": self.request.build_absolute_uri("/")}
|
||||
try:
|
||||
source: OAuthSource = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404(f"Unknown OAuth source '{slug}'.")
|
||||
else:
|
||||
payload = DEFAULT_PAYLOAD.copy()
|
||||
payload["X-Plex-Client-Identifier"] = source.consumer_key
|
||||
# Get a pin first
|
||||
pin = self.get_pin(**payload)
|
||||
LOGGER.debug("Got pin", **pin)
|
||||
self.request.session[SESSION_ID_KEY] = pin["id"]
|
||||
self.request.session[SESSION_CODE_KEY] = pin["code"]
|
||||
qs = {
|
||||
"clientID": source.consumer_key,
|
||||
"code": pin["code"],
|
||||
"forwardUrl": self.request.build_absolute_uri(
|
||||
self.get_callback_url(source)
|
||||
),
|
||||
}
|
||||
return f"https://app.plex.tv/auth#!?{urlencode(qs)}"
|
||||
|
||||
|
||||
class PlexOAuthClient(OAuth2Client):
|
||||
"""Retrive the plex token after authentication, then ask the plex API about user info"""
|
||||
|
||||
def check_application_state(self) -> bool:
|
||||
return SESSION_ID_KEY in self.request.session
|
||||
|
||||
def get_access_token(self, **request_kwargs) -> Optional[dict[str, Any]]:
|
||||
payload = dict(DEFAULT_PAYLOAD)
|
||||
payload["X-Plex-Client-Identifier"] = self.source.consumer_key
|
||||
payload["Accept"] = "application/json"
|
||||
response = get(
|
||||
f"https://plex.tv/api/v2/pins/{self.request.session[SESSION_ID_KEY]}",
|
||||
headers=payload,
|
||||
)
|
||||
response.raise_for_status()
|
||||
token = response.json()["authToken"]
|
||||
return {"plex_token": token}
|
||||
|
||||
def get_profile_info(self, token: dict[str, str]) -> Optional[dict[str, Any]]:
|
||||
"Fetch user profile information."
|
||||
qs = {"X-Plex-Token": token["plex_token"]}
|
||||
try:
|
||||
response = self.do_request(
|
||||
"get", f"https://plex.tv/users/account.json?{urlencode(qs)}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to fetch user profile", exc=exc)
|
||||
return None
|
||||
else:
|
||||
return response.json().get("user", {})
|
||||
|
||||
|
||||
class PlexOAuth2Callback(OAuthCallback):
|
||||
"""Plex OAuth2 Callback"""
|
||||
|
||||
client_class = PlexOAuthClient
|
||||
|
||||
def get_user_id(
|
||||
self, source: UserOAuthSourceConnection, info: dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
return info.get("uuid")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"username": info.get("username"),
|
||||
"email": info.get("email"),
|
||||
"name": info.get("title"),
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.type()
|
||||
class PlexType(SourceType):
|
||||
"""Plex Type definition"""
|
||||
|
||||
redirect_view = PlexRedirect
|
||||
callback_view = PlexOAuth2Callback
|
||||
name = "Plex"
|
||||
slug = "plex"
|
||||
|
||||
authorization_url = ""
|
||||
access_token_url = "" # nosec
|
||||
profile_url = ""
|
|
@ -4,7 +4,6 @@ from typing import Any
|
|||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
@ -36,8 +35,6 @@ class RedditOAuth2Callback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Twitter OAuth Views"""
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
|
||||
|
@ -11,8 +10,6 @@ class TwitterOAuthCallback(OAuthCallback):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
|
@ -4,35 +4,14 @@ from typing import Any, Optional
|
|||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow, in_memory_stage
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.sources.oauth.auth import AuthorizedServiceBackend
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.base import OAuthClientMixin
|
||||
from authentik.sources.oauth.views.flows import (
|
||||
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS,
|
||||
PostUserEnrollmentStage,
|
||||
)
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -40,8 +19,7 @@ LOGGER = get_logger()
|
|||
class OAuthCallback(OAuthClientMixin, View):
|
||||
"Base OAuth callback view."
|
||||
|
||||
source_id = None
|
||||
source = None
|
||||
source: OAuthSource
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse:
|
||||
|
@ -60,47 +38,27 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
# Fetch access token
|
||||
token = client.get_access_token()
|
||||
if token is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve token.")
|
||||
return self.handle_login_failure("Could not retrieve token.")
|
||||
if "error" in token:
|
||||
return self.handle_login_failure(self.source, token["error"])
|
||||
return self.handle_login_failure(token["error"])
|
||||
# Fetch profile info
|
||||
info = client.get_profile_info(token)
|
||||
if info is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve profile.")
|
||||
identifier = self.get_user_id(self.source, info)
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
identifier = self.get_user_id(raw_info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure(self.source, "Could not determine id.")
|
||||
return self.handle_login_failure("Could not determine id.")
|
||||
# Get or create access record
|
||||
defaults = {
|
||||
"access_token": token.get("access_token"),
|
||||
}
|
||||
existing = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source, identifier=identifier
|
||||
enroll_info = self.get_user_enroll_context(raw_info)
|
||||
sfm = OAuthSourceFlowManager(
|
||||
source=self.source,
|
||||
request=self.request,
|
||||
identifier=identifier,
|
||||
enroll_info=enroll_info,
|
||||
)
|
||||
|
||||
if existing.exists():
|
||||
connection = existing.first()
|
||||
connection.access_token = token.get("access_token")
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
|
||||
**defaults
|
||||
)
|
||||
else:
|
||||
connection = UserOAuthSourceConnection(
|
||||
source=self.source,
|
||||
identifier=identifier,
|
||||
access_token=token.get("access_token"),
|
||||
)
|
||||
user = AuthorizedServiceBackend().authenticate(
|
||||
source=self.source, identifier=identifier, request=request
|
||||
return sfm.get_flow(
|
||||
access_token=token.get("access_token"),
|
||||
)
|
||||
if user is None:
|
||||
if self.request.user.is_authenticated:
|
||||
LOGGER.debug("Linking existing user", source=self.source)
|
||||
return self.handle_existing_user_link(self.source, connection, info)
|
||||
LOGGER.debug("Handling enrollment of new user", source=self.source)
|
||||
return self.handle_enroll(self.source, connection, info)
|
||||
LOGGER.debug("Handling existing user", source=self.source)
|
||||
return self.handle_existing_user(self.source, user, connection, info)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_callback_url(self, source: OAuthSource) -> str:
|
||||
|
@ -114,132 +72,35 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Create a dict of User data"""
|
||||
raise NotImplementedError()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_user_id(
|
||||
self, source: UserOAuthSourceConnection, info: dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
|
||||
"""Return unique identifier from the profile info."""
|
||||
if "id" in info:
|
||||
return info["id"]
|
||||
return None
|
||||
|
||||
def handle_login_failure(self, source: OAuthSource, reason: str) -> HttpResponse:
|
||||
def handle_login_failure(self, reason: str) -> HttpResponse:
|
||||
"Message user and redirect on error."
|
||||
LOGGER.warning("Authentication Failure", reason=reason)
|
||||
messages.error(self.request, _("Authentication Failed."))
|
||||
return redirect(self.get_error_redirect(source, reason))
|
||||
return redirect(self.get_error_redirect(self.source, reason))
|
||||
|
||||
def handle_login_flow(
|
||||
self, flow: Flow, *stages_to_append, **kwargs
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-admin"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
}
|
||||
)
|
||||
if not flow:
|
||||
return HttpResponseBadRequest()
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in stages_to_append:
|
||||
plan.append(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_existing_user(
|
||||
class OAuthSourceFlowManager(SourceFlowManager):
|
||||
"""Flow manager for oauth sources"""
|
||||
|
||||
connection_type = UserOAuthSourceConnection
|
||||
|
||||
def update_connection(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
user: User,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> HttpResponse:
|
||||
"Login user and redirect."
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: user}
|
||||
return self.handle_login_flow(source.authentication_flow, **flow_kwargs)
|
||||
|
||||
def handle_existing_user_link(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> HttpResponse:
|
||||
"""Handler when the user was already authenticated and linked an external source
|
||||
to their account."""
|
||||
# there's already a user logged in, just link them up
|
||||
user = self.request.user
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
Event.new(
|
||||
EventAction.SOURCE_LINKED, message="Linked OAuth Source", source=source
|
||||
).from_http(self.request)
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_core:if-admin",
|
||||
)
|
||||
+ f"#/user;page-{self.source.slug}"
|
||||
)
|
||||
|
||||
def handle_enroll(
|
||||
self,
|
||||
source: OAuthSource,
|
||||
access: UserOAuthSourceConnection,
|
||||
info: dict[str, Any],
|
||||
) -> HttpResponse:
|
||||
"""User was not authenticated and previous request was not authenticated."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
if not source.enrollment_flow:
|
||||
LOGGER.warning("source has no enrollment flow", source=source)
|
||||
return HttpResponseBadRequest()
|
||||
return self.handle_login_flow(
|
||||
source.enrollment_flow,
|
||||
in_memory_stage(PostUserEnrollmentStage),
|
||||
**{
|
||||
PLAN_CONTEXT_PROMPT: delete_none_keys(
|
||||
self.get_user_enroll_context(source, access, info)
|
||||
),
|
||||
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access,
|
||||
},
|
||||
)
|
||||
connection: UserOAuthSourceConnection,
|
||||
access_token: Optional[str] = None,
|
||||
) -> UserOAuthSourceConnection:
|
||||
"""Set the access_token on the connection"""
|
||||
connection.access_token = access_token
|
||||
return connection
|
||||
|
|
0
authentik/sources/plex/__init__.py
Normal file
0
authentik/sources/plex/__init__.py
Normal file
75
authentik/sources/plex/api.py
Normal file
75
authentik/sources/plex/api.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
"""Plex Source Serializer"""
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField
|
||||
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 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
|
||||
|
||||
|
||||
class PlexSourceSerializer(SourceSerializer):
|
||||
"""Plex Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = PlexSource
|
||||
fields = SourceSerializer.Meta.fields + ["client_id", "allowed_servers"]
|
||||
|
||||
|
||||
class PlexTokenRedeemSerializer(PassiveSerializer):
|
||||
"""Serializer to redeem a plex token"""
|
||||
|
||||
plex_token = CharField()
|
||||
|
||||
|
||||
class PlexSourceViewSet(ModelViewSet):
|
||||
"""Plex source Viewset"""
|
||||
|
||||
queryset = PlexSource.objects.all()
|
||||
serializer_class = PlexSourceSerializer
|
||||
lookup_field = "slug"
|
||||
|
||||
@permission_required(None)
|
||||
@swagger_auto_schema(
|
||||
request_body=PlexTokenRedeemSerializer(),
|
||||
responses={200: RedirectChallenge(), 404: "Token not found"},
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name="slug",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_STRING,
|
||||
)
|
||||
],
|
||||
)
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
def redeem_token(self, request: Request) -> Response:
|
||||
"""Redeem a plex token, check it's access to resources against what's allowed
|
||||
for the source, and redirect to an authentication/enrollment flow."""
|
||||
source: PlexSource = get_object_or_404(
|
||||
PlexSource, slug=request.query_params.get("slug", "")
|
||||
)
|
||||
plex_token = request.data.get("plex_token", None)
|
||||
if not plex_token:
|
||||
raise Http404
|
||||
auth_api = PlexAuth(source, plex_token)
|
||||
if not auth_api.check_server_overlap():
|
||||
raise Http404
|
||||
response = auth_api.get_user_url(request)
|
||||
return to_stage_response(request, response)
|
10
authentik/sources/plex/apps.py
Normal file
10
authentik/sources/plex/apps.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""authentik plex config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikSourcePlexConfig(AppConfig):
|
||||
"""authentik source plex config"""
|
||||
|
||||
name = "authentik.sources.plex"
|
||||
label = "authentik_sources_plex"
|
||||
verbose_name = "authentik Sources.Plex"
|
77
authentik/sources/plex/migrations/0001_initial.py
Normal file
77
authentik/sources/plex/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
# Generated by Django 3.2 on 2021-05-03 18:59
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0020_source_user_matching_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PlexSource",
|
||||
fields=[
|
||||
(
|
||||
"source_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.source",
|
||||
),
|
||||
),
|
||||
(
|
||||
"client_id",
|
||||
models.TextField(
|
||||
default="yOuPQQvgNfBGreZZ38WoOY1d3qk3Xso2AuQHi6RG",
|
||||
help_text="Client identifier used to talk to Plex.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"allowed_servers",
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Plex Source",
|
||||
"verbose_name_plural": "Plex Sources",
|
||||
},
|
||||
bases=("authentik_core.source",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PlexSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"usersourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.usersourceconnection",
|
||||
),
|
||||
),
|
||||
("plex_token", models.TextField()),
|
||||
("identifier", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Plex Source Connection",
|
||||
"verbose_name_plural": "User Plex Source Connections",
|
||||
},
|
||||
bases=("authentik_core.usersourceconnection",),
|
||||
),
|
||||
]
|
0
authentik/sources/plex/migrations/__init__.py
Normal file
0
authentik/sources/plex/migrations/__init__.py
Normal file
80
authentik/sources/plex/models.py
Normal file
80
authentik/sources/plex/models.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""Plex source"""
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.flows.challenge import Challenge, ChallengeTypes
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
|
||||
|
||||
class PlexAuthenticationChallenge(Challenge):
|
||||
"""Challenge shown to the user in identification stage"""
|
||||
|
||||
client_id = CharField()
|
||||
slug = CharField()
|
||||
|
||||
|
||||
class PlexSource(Source):
|
||||
"""Authenticate against plex.tv"""
|
||||
|
||||
client_id = models.TextField(
|
||||
default=generate_client_id(),
|
||||
help_text=_("Client identifier used to talk to Plex."),
|
||||
)
|
||||
allowed_servers = ArrayField(
|
||||
models.TextField(),
|
||||
default=list,
|
||||
help_text=_(
|
||||
(
|
||||
"Which servers a user has to be a member of to be granted access. "
|
||||
"Empty list allows every server."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-source-plex-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.sources.plex.api import PlexSourceSerializer
|
||||
|
||||
return PlexSourceSerializer
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
challenge=PlexAuthenticationChallenge(
|
||||
{
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"component": "ak-flow-sources-plex",
|
||||
"client_id": self.client_id,
|
||||
"slug": self.slug,
|
||||
}
|
||||
),
|
||||
icon_url=static("authentik/sources/plex.svg"),
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Plex Source")
|
||||
verbose_name_plural = _("Plex Sources")
|
||||
|
||||
|
||||
class PlexSourceConnection(UserSourceConnection):
|
||||
"""Connect user and plex source"""
|
||||
|
||||
plex_token = models.TextField()
|
||||
identifier = models.TextField()
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("User Plex Source Connection")
|
||||
verbose_name_plural = _("User Plex Source Connections")
|
112
authentik/sources/plex/plex.py
Normal file
112
authentik/sources/plex/plex.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""Plex Views"""
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import Http404, HttpResponse
|
||||
from requests import Session
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_ID_KEY = "PLEX_ID"
|
||||
SESSION_CODE_KEY = "PLEX_CODE"
|
||||
|
||||
|
||||
class PlexAuth:
|
||||
"""Plex authentication utilities"""
|
||||
|
||||
_source: PlexSource
|
||||
_token: str
|
||||
|
||||
def __init__(self, source: PlexSource, token: str):
|
||||
self._source = source
|
||||
self._token = token
|
||||
self._session = Session()
|
||||
self._session.headers.update(
|
||||
{"Accept": "application/json", "Content-Type": "application/json"}
|
||||
)
|
||||
self._session.headers.update(self.headers)
|
||||
|
||||
@property
|
||||
def headers(self) -> dict[str, str]:
|
||||
"""Get common headers"""
|
||||
return {
|
||||
"X-Plex-Product": "authentik",
|
||||
"X-Plex-Version": __version__,
|
||||
"X-Plex-Device-Vendor": "BeryJu.org",
|
||||
}
|
||||
|
||||
def get_resources(self) -> list[dict]:
|
||||
"""Get all resources the plex-token has access to"""
|
||||
qs = {
|
||||
"X-Plex-Token": self._token,
|
||||
"X-Plex-Client-Identifier": self._source.client_id,
|
||||
}
|
||||
response = self._session.get(
|
||||
f"https://plex.tv/api/v2/resources?{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 = {
|
||||
"X-Plex-Token": self._token,
|
||||
"X-Plex-Client-Identifier": self._source.client_id,
|
||||
}
|
||||
response = self._session.get(
|
||||
f"https://plex.tv/api/v2/user?{urlencode(qs)}",
|
||||
)
|
||||
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")
|
||||
|
||||
def check_server_overlap(self) -> bool:
|
||||
"""Check if the plex-token has any server overlap with our configured servers"""
|
||||
try:
|
||||
resources = self.get_resources()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to fetch user resources", exc=exc)
|
||||
raise Http404
|
||||
else:
|
||||
for resource in resources:
|
||||
if resource["provides"] != "server":
|
||||
continue
|
||||
if resource["clientIdentifier"] in self._source.allowed_servers:
|
||||
LOGGER.info(
|
||||
"Plex allowed access from server", name=resource["name"]
|
||||
)
|
||||
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"""
|
||||
|
||||
connection_type = PlexSourceConnection
|
||||
|
||||
def update_connection(
|
||||
self, connection: PlexSourceConnection, plex_token: str
|
||||
) -> PlexSourceConnection:
|
||||
"""Set the access_token on the connection"""
|
||||
connection.plex_token = plex_token
|
||||
return connection
|
64
authentik/sources/plex/tests.py
Normal file
64
authentik/sources/plex/tests.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""plex Source tests"""
|
||||
from django.test import TestCase
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.sources.plex.models import PlexSource
|
||||
from authentik.sources.plex.plex import PlexAuth
|
||||
|
||||
USER_INFO_RESPONSE = {
|
||||
"id": 1234123419,
|
||||
"uuid": "qwerqewrqewrqwr",
|
||||
"username": "username",
|
||||
"title": "title",
|
||||
"email": "foo@bar.baz",
|
||||
}
|
||||
RESOURCES_RESPONSE = [
|
||||
{
|
||||
"name": "foo",
|
||||
"clientIdentifier": "allowed",
|
||||
"provides": "server",
|
||||
},
|
||||
{
|
||||
"name": "foo",
|
||||
"clientIdentifier": "denied",
|
||||
"provides": "server",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestPlexSource(TestCase):
|
||||
"""plex Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source: PlexSource = PlexSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
)
|
||||
|
||||
def test_get_user_info(self):
|
||||
"""Test get_user_info"""
|
||||
token = generate_client_secret()
|
||||
api = PlexAuth(self.source, token)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE)
|
||||
self.assertEqual(
|
||||
api.get_user_info(),
|
||||
(
|
||||
{"username": "username", "email": "foo@bar.baz", "name": "title"},
|
||||
1234123419,
|
||||
),
|
||||
)
|
||||
|
||||
def test_check_server_overlap(self):
|
||||
"""Test check_server_overlap"""
|
||||
token = generate_client_secret()
|
||||
api = PlexAuth(self.source, token)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
|
||||
self.assertFalse(api.check_server_overlap())
|
||||
self.source.allowed_servers = ["allowed"]
|
||||
self.source.save()
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
|
||||
self.assertTrue(api.check_server_overlap())
|
|
@ -10,6 +10,7 @@ from rest_framework.serializers import Serializer
|
|||
from authentik.core.models import Source
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
|
@ -169,10 +170,16 @@ class SAMLSource(Source):
|
|||
@property
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
name=self.name,
|
||||
url=reverse(
|
||||
"authentik_sources_saml:login", kwargs={"source_slug": self.slug}
|
||||
challenge=RedirectChallenge(
|
||||
instance={
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": reverse(
|
||||
"authentik_sources_saml:login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
}
|
||||
),
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -115,7 +115,7 @@ class InitiateView(View):
|
|||
# Encode it back into a string
|
||||
res = ParseResult(
|
||||
scheme=sso_url.scheme,
|
||||
netloc=sso_url.hostname or "",
|
||||
netloc=sso_url.netloc,
|
||||
path=sso_url.path,
|
||||
params=sso_url.params,
|
||||
query=urlencode(url_kwargs),
|
||||
|
|
|
@ -48,7 +48,13 @@ def send_mail(
|
|||
stage: EmailStage = EmailStage(use_global_settings=True)
|
||||
else:
|
||||
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
|
||||
backend = stage.backend
|
||||
try:
|
||||
backend = stage.backend
|
||||
except ValueError as exc:
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
LOGGER.warning(exc)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
return
|
||||
backend.open()
|
||||
# Since django's EmailMessage objects are not JSON serialisable,
|
||||
# we need to rebuild them from a dict
|
||||
|
@ -68,7 +74,7 @@ def send_mail(
|
|||
messages=["Successfully sent Mail."],
|
||||
)
|
||||
)
|
||||
except (SMTPException, ConnectionError, ValueError) as exc:
|
||||
except (SMTPException, ConnectionError) as exc:
|
||||
LOGGER.debug("Error sending email, retrying...", exc=exc)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
raise exc
|
||||
|
|
|
@ -112,7 +112,9 @@ class IdentificationStageView(ChallengeStageView):
|
|||
for source in sources:
|
||||
ui_login_button = source.ui_login_button
|
||||
if ui_login_button:
|
||||
ui_sources.append(asdict(ui_login_button))
|
||||
button = asdict(ui_login_button)
|
||||
button["challenge"] = ui_login_button.challenge.data
|
||||
ui_sources.append(button)
|
||||
challenge.initial_data["sources"] = ui_sources
|
||||
return challenge
|
||||
|
||||
|
|
|
@ -117,7 +117,10 @@ class TestIdentificationStage(TestCase):
|
|||
{
|
||||
"icon_url": "/static/authentik/sources/.svg",
|
||||
"name": "test",
|
||||
"url": "/source/oauth/login/test/",
|
||||
"challenge": {
|
||||
"to": "/source/oauth/login/test/",
|
||||
"type": "redirect",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -158,9 +161,12 @@ class TestIdentificationStage(TestCase):
|
|||
"title": self.flow.title,
|
||||
"sources": [
|
||||
{
|
||||
"challenge": {
|
||||
"to": "/source/oauth/login/test/",
|
||||
"type": "redirect",
|
||||
},
|
||||
"icon_url": "/static/authentik/sources/.svg",
|
||||
"name": "test",
|
||||
"url": "/source/oauth/login/test/",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
|
|
@ -39,6 +39,7 @@ class InvitationSerializer(ModelSerializer):
|
|||
"expires",
|
||||
"fixed_data",
|
||||
"created_by",
|
||||
"single_use",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.2 on 2021-05-03 07:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_invitation", "0003_auto_20201227_1210"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="invitation",
|
||||
name="single_use",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, the invitation will be deleted after usage.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -53,6 +53,11 @@ class Invitation(models.Model):
|
|||
|
||||
invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
single_use = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("When enabled, the invitation will be deleted after usage."),
|
||||
)
|
||||
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
expires = models.DateTimeField(default=None, blank=True, null=True)
|
||||
fixed_data = models.JSONField(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""invitation stage logic"""
|
||||
from copy import deepcopy
|
||||
from typing import Optional
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
@ -38,7 +39,9 @@ class InvitationStageView(StageView):
|
|||
return self.executor.stage_invalid()
|
||||
|
||||
invite: Invitation = get_object_or_404(Invitation, pk=token)
|
||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data
|
||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data)
|
||||
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
||||
invitation_used.send(sender=self, request=request, invitation=invite)
|
||||
if invite.single_use:
|
||||
invite.delete()
|
||||
return self.executor.stage_ok()
|
||||
|
|
|
@ -130,7 +130,7 @@ class TestUserLoginStage(TestCase):
|
|||
"""Test with invitation, check data in session"""
|
||||
data = {"foo": "bar"}
|
||||
invite = Invitation.objects.create(
|
||||
created_by=get_anonymous_user(), fixed_data=data
|
||||
created_by=get_anonymous_user(), fixed_data=data, single_use=True
|
||||
)
|
||||
|
||||
plan = FlowPlan(
|
||||
|
@ -156,6 +156,7 @@ class TestUserLoginStage(TestCase):
|
|||
force_str(response.content),
|
||||
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
|
||||
)
|
||||
self.assertFalse(Invitation.objects.filter(pk=invite.pk))
|
||||
|
||||
|
||||
class TestInvitationsAPI(APITestCase):
|
||||
|
|
|
@ -100,7 +100,7 @@ stages:
|
|||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: npm install -g pyright@1.1.109
|
||||
script: npm install -g pyright@1.1.136
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
|
@ -201,7 +201,7 @@ stages:
|
|||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run make coverage
|
||||
pipenv run make test
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
|
@ -371,12 +371,36 @@ stages:
|
|||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: bash <(curl -s https://codecov.io/bash)
|
||||
- stage: generate
|
||||
jobs:
|
||||
- job: swagger_generate
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: 'web/api/'
|
||||
artifact: 'ts_swagger_client'
|
||||
publishLocation: 'pipeline'
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: build_server
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'ts_swagger_client'
|
||||
path: "web/api/"
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
|
|
52
cmd/server/main.go
Normal file
52
cmd/server/main.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/gounicorn"
|
||||
"goauthentik.io/internal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
config.DefaultConfig()
|
||||
config.LoadConfig("./authentik/lib/default.yml")
|
||||
config.LoadConfig("./local.env.yml")
|
||||
config.ConfigureLogger()
|
||||
|
||||
if config.G.ErrorReporting.Enabled {
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
AttachStacktrace: true,
|
||||
TracesSampleRate: 0.6,
|
||||
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
||||
Environment: config.G.ErrorReporting.Environment,
|
||||
})
|
||||
defer sentry.Flush(time.Second * 5)
|
||||
defer sentry.Recover()
|
||||
}
|
||||
|
||||
rl := log.WithField("logger", "authentik.g")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
g := gounicorn.NewGoUnicorn()
|
||||
for {
|
||||
err := g.Start()
|
||||
rl.WithError(err).Warning("gunicorn process died, restarting")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ws := web.NewWebServer()
|
||||
ws.Run()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
|
@ -17,6 +17,7 @@ services:
|
|||
- .env
|
||||
redis:
|
||||
image: redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
|
@ -44,9 +45,12 @@ services:
|
|||
traefik.http.routers.app-router.service: app-service
|
||||
traefik.http.routers.app-router.tls: 'true'
|
||||
traefik.http.services.app-service.loadbalancer.healthcheck.path: /-/health/live/
|
||||
traefik.http.services.app-service.loadbalancer.server.port: '8000'
|
||||
traefik.http.services.app-service.loadbalancer.server.port: '9000'
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.4.5}
|
||||
restart: unless-stopped
|
||||
|
@ -67,39 +71,6 @@ services:
|
|||
- geoip:/geoip
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: ${AUTHENTIK_IMAGE_STATIC:-beryju/authentik-static}:${AUTHENTIK_TAG:-2021.4.5}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
traefik.enable: 'true'
|
||||
traefik.docker.network: internal
|
||||
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/if`, `/media`, `/robots.txt`, `/favicon.ico`)
|
||||
traefik.http.routers.static-router.tls: 'true'
|
||||
traefik.http.routers.static-router.service: static-service
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.interval: 30s
|
||||
traefik.http.services.static-service.loadbalancer.server.port: '80'
|
||||
volumes:
|
||||
- ./media:/usr/share/nginx/html/media
|
||||
traefik:
|
||||
image: traefik:2.3
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--log.format=json"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.http.address=:80"
|
||||
- "--entrypoints.https.address=:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- "0.0.0.0:443:443"
|
||||
- "127.0.0.1:8080:8080"
|
||||
networks:
|
||||
- internal
|
||||
geoipupdate:
|
||||
image: "maxmindinc/geoipupdate:latest"
|
||||
volumes:
|
||||
|
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
|||
module goauthentik.io
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/getsentry/sentry-go v0.10.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
190
go.sum
Normal file
190
go.sum
Normal file
|
@ -0,0 +1,190 @@
|
|||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g=
|
||||
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -32,24 +32,4 @@ spec:
|
|||
backend:
|
||||
serviceName: {{ $fullName }}-web
|
||||
servicePort: http
|
||||
- path: /static/
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /if/
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /media/
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /robots.txt
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /favicon.ico
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "authentik.fullname" . }}-static
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
helm.sh/chart: {{ include "authentik.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.goauthentik.io/component: static
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.goauthentik.io/component: static
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.goauthentik.io/component: static
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-static
|
||||
image: "{{ .Values.image.name_static }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 10M
|
||||
limits:
|
||||
cpu: 20m
|
||||
memory: 20M
|
||||
volumeMounts:
|
||||
- name: authentik-uploads
|
||||
mountPath: /usr/share/nginx/html/media
|
||||
volumes:
|
||||
- name: authentik-uploads
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "authentik.fullname" . }}-uploads
|
|
@ -1,21 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "authentik.fullname" . }}-static
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
helm.sh/chart: {{ include "authentik.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.goauthentik.io/component: static
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.goauthentik.io/component: static
|
|
@ -1,17 +0,0 @@
|
|||
{{- if .Values.monitoring.enabled -}}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "authentik.name" . }}
|
||||
helm.sh/chart: {{ include "authentik.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
name: {{ include "authentik.fullname" . }}-static-monitoring
|
||||
spec:
|
||||
endpoints:
|
||||
- port: http
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s.goauthentik.io/component: static
|
||||
{{- end }}
|
|
@ -79,7 +79,10 @@ spec:
|
|||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
containerPort: 9000
|
||||
protocol: TCP
|
||||
- name: https
|
||||
containerPort: 9443
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
|
@ -11,7 +11,7 @@ metadata:
|
|||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
- port: 9000
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
|
|
76
internal/config/config.go
Normal file
76
internal/config/config.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var G Config
|
||||
|
||||
func DefaultConfig() {
|
||||
G = Config{
|
||||
Debug: false,
|
||||
Web: WebConfig{
|
||||
Listen: "localhost:9000",
|
||||
ListenTLS: "localhost:9443",
|
||||
},
|
||||
Paths: PathsConfig{
|
||||
Media: "./media",
|
||||
},
|
||||
LogLevel: "info",
|
||||
ErrorReporting: ErrorReportingConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(path string) error {
|
||||
raw, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to load config file")
|
||||
}
|
||||
rawExpanded := os.ExpandEnv(string(raw))
|
||||
nc := Config{}
|
||||
err = yaml.Unmarshal([]byte(rawExpanded), &nc)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse YAML")
|
||||
}
|
||||
if err := mergo.Merge(&G, nc, mergo.WithOverride); err != nil {
|
||||
return errors.Wrap(err, "failed to overlay config")
|
||||
}
|
||||
log.WithField("path", path).Debug("Loaded config")
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigureLogger() {
|
||||
switch G.LogLevel {
|
||||
case "trace":
|
||||
log.SetLevel(log.TraceLevel)
|
||||
case "debug":
|
||||
log.SetLevel(log.DebugLevel)
|
||||
case "info":
|
||||
log.SetLevel(log.InfoLevel)
|
||||
case "warning":
|
||||
log.SetLevel(log.WarnLevel)
|
||||
case "error":
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
default:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
if G.Debug {
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
} else {
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
FieldMap: log.FieldMap{
|
||||
log.FieldKeyMsg: "event",
|
||||
log.FieldKeyTime: "timestamp",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
24
internal/config/struct.go
Normal file
24
internal/config/struct.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
Debug bool `yaml:"debug"`
|
||||
Web WebConfig `yaml:"web"`
|
||||
Paths PathsConfig `yaml:"paths"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
ErrorReporting ErrorReportingConfig `yaml:"error_reporting"`
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
Listen string `yaml:"listen"`
|
||||
ListenTLS string `yaml:"listen_tls"`
|
||||
}
|
||||
|
||||
type PathsConfig struct {
|
||||
Media string `yaml:"media"`
|
||||
}
|
||||
|
||||
type ErrorReportingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Environment string `yaml:"environment"`
|
||||
SendPII bool `yaml:"send_pii"`
|
||||
}
|
3
internal/constants/constants.go
Normal file
3
internal/constants/constants.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package constants
|
||||
|
||||
const VERSION = "2021.4.5"
|
63
internal/crypto/generate.go
Normal file
63
internal/crypto/generate.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GenerateSelfSignedCert Generate a self-signed TLS Certificate, to be used as fallback
|
||||
func GenerateSelfSignedCert() (tls.Certificate, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate private key: %v", err)
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate serial number: %v", err)
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"authentik"},
|
||||
CommonName: "authentik default certificate",
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: keyUsage,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
template.DNSNames = []string{"*"}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
log.Warning(err)
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
log.Warning(err)
|
||||
}
|
||||
privPemByes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
||||
return tls.X509KeyPair(pemBytes, privPemByes)
|
||||
}
|
36
internal/gounicorn/gounicorn.go
Normal file
36
internal/gounicorn/gounicorn.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package gounicorn
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
)
|
||||
|
||||
type GoUnicorn struct {
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
func NewGoUnicorn() *GoUnicorn {
|
||||
return &GoUnicorn{
|
||||
log: log.WithField("logger", "authentik.g.unicorn"),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoUnicorn) Start() error {
|
||||
command := "gunicorn"
|
||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
||||
if config.G.Debug {
|
||||
command = "python"
|
||||
args = []string{"manage.py", "runserver", "localhost:8000"}
|
||||
}
|
||||
g.log.WithField("args", args).WithField("cmd", command).Debug("Starting gunicorn")
|
||||
p := exec.Command(command, args...)
|
||||
p.Env = append(os.Environ(),
|
||||
"WORKERS=2",
|
||||
)
|
||||
p.Stdout = os.Stdout
|
||||
p.Stderr = os.Stderr
|
||||
return p.Run()
|
||||
}
|
25
internal/web/middleware_log.go
Normal file
25
internal/web/middleware_log.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
span := sentry.StartSpan(r.Context(), "request.logging")
|
||||
before := time.Now()
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
after := time.Now()
|
||||
log.WithFields(log.Fields{
|
||||
"remote": r.RemoteAddr,
|
||||
"method": r.Method,
|
||||
"took": after.Sub(before),
|
||||
}).Info(r.RequestURI)
|
||||
span.Finish()
|
||||
})
|
||||
}
|
38
internal/web/middleware_sentry.go
Normal file
38
internal/web/middleware_sentry.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
)
|
||||
|
||||
func recoveryMiddleware() func(next http.Handler) http.Handler {
|
||||
sentryHandler := sentryhttp.New(sentryhttp.Options{})
|
||||
return func(next http.Handler) http.Handler {
|
||||
sentryHandler.Handle(next)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
defer func() {
|
||||
re := recover()
|
||||
if re == nil {
|
||||
return
|
||||
}
|
||||
err := re.(error)
|
||||
if err != nil {
|
||||
jsonBody, _ := json.Marshal(struct {
|
||||
Successful bool
|
||||
Error string
|
||||
}{
|
||||
Successful: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write(jsonBody)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
101
internal/web/web.go
Normal file
101
internal/web/web.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
)
|
||||
|
||||
type WebServer struct {
|
||||
Bind string
|
||||
BindTLS bool
|
||||
|
||||
LegacyProxy bool
|
||||
|
||||
stop chan struct{} // channel for waiting shutdown
|
||||
|
||||
m *mux.Router
|
||||
lh *mux.Router
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
func NewWebServer() *WebServer {
|
||||
mainHandler := mux.NewRouter()
|
||||
if config.G.ErrorReporting.Enabled {
|
||||
mainHandler.Use(recoveryMiddleware())
|
||||
}
|
||||
mainHandler.Use(handlers.ProxyHeaders)
|
||||
mainHandler.Use(handlers.CompressHandler)
|
||||
logginRouter := mainHandler.NewRoute().Subrouter()
|
||||
logginRouter.Use(loggingMiddleware)
|
||||
|
||||
ws := &WebServer{
|
||||
LegacyProxy: true,
|
||||
|
||||
m: mainHandler,
|
||||
lh: logginRouter,
|
||||
log: log.WithField("logger", "authentik.g.web"),
|
||||
}
|
||||
ws.configureStatic()
|
||||
ws.configureProxy()
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws *WebServer) Run() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ws.listenPlain()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ws.listenTLS()
|
||||
}()
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func (ws *WebServer) listenPlain() {
|
||||
ln, err := net.Listen("tcp", config.G.Web.Listen)
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Fatalf("failed to listen")
|
||||
}
|
||||
ws.log.WithField("addr", config.G.Web.Listen).Info("Running")
|
||||
|
||||
ws.serve(ln)
|
||||
|
||||
ws.log.WithField("addr", config.G.Web.Listen).Info("Running")
|
||||
http.ListenAndServe(config.G.Web.Listen, ws.m)
|
||||
}
|
||||
|
||||
func (ws *WebServer) serve(listener net.Listener) {
|
||||
srv := &http.Server{
|
||||
Handler: ws.m,
|
||||
}
|
||||
|
||||
// See https://golang.org/pkg/net/http/#Server.Shutdown
|
||||
idleConnsClosed := make(chan struct{})
|
||||
go func() {
|
||||
<-ws.stop // wait notification for stopping server
|
||||
|
||||
// We received an interrupt signal, shut down.
|
||||
if err := srv.Shutdown(context.Background()); err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
ws.log.Printf("HTTP server Shutdown: %v", err)
|
||||
}
|
||||
close(idleConnsClosed)
|
||||
}()
|
||||
|
||||
err := srv.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
ws.log.Errorf("ERROR: http.Serve() - %s", err)
|
||||
}
|
||||
<-idleConnsClosed
|
||||
}
|
26
internal/web/web_proxy.go
Normal file
26
internal/web/web_proxy.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (ws *WebServer) configureProxy() {
|
||||
// Reverse proxy to the application server
|
||||
u, _ := url.Parse("http://localhost:8000")
|
||||
rp := httputil.NewSingleHostReverseProxy(u)
|
||||
rp.ErrorHandler = ws.proxyErrorHandler
|
||||
rp.ModifyResponse = ws.proxyModifyResponse
|
||||
ws.m.PathPrefix("/").Handler(rp)
|
||||
}
|
||||
|
||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
ws.log.WithError(err).Warning("proxy error")
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
||||
func (ws *WebServer) proxyModifyResponse(r *http.Response) error {
|
||||
r.Header.Set("X-authentik-from", "authentik")
|
||||
return nil
|
||||
}
|
32
internal/web/web_ssl.go
Normal file
32
internal/web/web_ssl.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/crypto"
|
||||
)
|
||||
|
||||
// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests
|
||||
func (ws *WebServer) listenTLS() {
|
||||
cert, err := crypto.GenerateSelfSignedCert()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Error("failed to generate default cert")
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", config.G.Web.ListenTLS)
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Fatalf("failed to listen")
|
||||
}
|
||||
ws.log.WithField("addr", config.G.Web.ListenTLS).Info("Running")
|
||||
|
||||
tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig)
|
||||
ws.serve(tlsListener)
|
||||
ws.log.Printf("closing %s", tlsListener.Addr())
|
||||
}
|
43
internal/web/web_static.go
Normal file
43
internal/web/web_static.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"goauthentik.io/internal/config"
|
||||
staticWeb "goauthentik.io/web"
|
||||
)
|
||||
|
||||
func (ws *WebServer) configureStatic() {
|
||||
if config.G.Debug {
|
||||
ws.log.Debug("Using local static files")
|
||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||
} else {
|
||||
ws.log.Debug("Using packaged static files")
|
||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
|
||||
}
|
||||
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header()["Content-Type"] = []string{"text/plain"}
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(staticWeb.RobotsTxt)
|
||||
})
|
||||
ws.lh.Path("/.well-known/security.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header()["Content-Type"] = []string{"text/plain"}
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(staticWeb.SecurityTxt)
|
||||
})
|
||||
// Interfaces
|
||||
ws.lh.Path("/if/admin/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header()["Content-Type"] = []string{"text/html"}
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(staticWeb.InterfaceAdmin)
|
||||
})
|
||||
ws.lh.Path("/if/flow/{slug}/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header()["Content-Type"] = []string{"text/html"}
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(staticWeb.InterfaceFlow)
|
||||
})
|
||||
// Media files, always local
|
||||
ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media))))
|
||||
}
|
31
internal/web/web_utils.go
Normal file
31
internal/web/web_utils.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tc.SetKeepAlive(true)
|
||||
if err != nil {
|
||||
log.Printf("Error setting Keep-Alive: %v", err)
|
||||
}
|
||||
err = tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error setting Keep-Alive period: %v", err)
|
||||
}
|
||||
return tc, nil
|
||||
}
|
|
@ -3,7 +3,7 @@ python -m lifecycle.wait_for_db
|
|||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr
|
||||
if [[ "$1" == "server" ]]; then
|
||||
python -m lifecycle.migrate
|
||||
gunicorn -c /lifecycle/gunicorn.conf.py authentik.root.asgi:application
|
||||
/authentik-proxy
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
celery -A authentik.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events
|
||||
elif [[ "$1" == "migrate" ]]; then
|
||||
|
|
|
@ -6,7 +6,7 @@ from multiprocessing import cpu_count
|
|||
import structlog
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
|
||||
bind = "0.0.0.0:8000"
|
||||
bind = "127.0.0.1:8000"
|
||||
|
||||
user = "authentik"
|
||||
group = "authentik"
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
423
swagger.yaml
423
swagger.yaml
|
@ -2021,6 +2021,11 @@ paths:
|
|||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- name: attributes
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
|
@ -10534,6 +10539,238 @@ paths:
|
|||
description: A unique integer value identifying this User OAuth Source Connection.
|
||||
required: true
|
||||
type: integer
|
||||
/sources/plex/:
|
||||
get:
|
||||
operationId: sources_plex_list
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
required: false
|
||||
type: string
|
||||
- name: search
|
||||
in: query
|
||||
description: A search term.
|
||||
required: false
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
description: Page Index
|
||||
required: false
|
||||
type: integer
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Page Size
|
||||
required: false
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
required:
|
||||
- results
|
||||
- pagination
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
required:
|
||||
- next
|
||||
- previous
|
||||
- count
|
||||
- current
|
||||
- total_pages
|
||||
- start_index
|
||||
- end_index
|
||||
type: object
|
||||
properties:
|
||||
next:
|
||||
type: number
|
||||
previous:
|
||||
type: number
|
||||
count:
|
||||
type: number
|
||||
current:
|
||||
type: number
|
||||
total_pages:
|
||||
type: number
|
||||
start_index:
|
||||
type: number
|
||||
end_index:
|
||||
type: number
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- sources
|
||||
post:
|
||||
operationId: sources_plex_create
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'201':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- sources
|
||||
parameters: []
|
||||
/sources/plex/redeem_token/:
|
||||
post:
|
||||
operationId: sources_plex_redeem_token
|
||||
description: |-
|
||||
Redeem a plex token, check it's access to resources against what's allowed
|
||||
for the source, and redirect to an authentication/enrollment flow.
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexTokenRedeem'
|
||||
- name: slug
|
||||
in: query
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/RedirectChallenge'
|
||||
'404':
|
||||
description: Token not found
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- sources
|
||||
parameters: []
|
||||
/sources/plex/{slug}/:
|
||||
get:
|
||||
operationId: sources_plex_read
|
||||
description: Plex source Viewset
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
put:
|
||||
operationId: sources_plex_update
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
patch:
|
||||
operationId: sources_plex_partial_update
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
delete:
|
||||
operationId: sources_plex_delete
|
||||
description: Plex source Viewset
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: ''
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
parameters:
|
||||
- name: slug
|
||||
in: path
|
||||
description: Internal source name, used in URLs.
|
||||
required: true
|
||||
type: string
|
||||
format: slug
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
/sources/saml/:
|
||||
get:
|
||||
operationId: sources_saml_list
|
||||
|
@ -16578,6 +16815,7 @@ definitions:
|
|||
- authentik.recovery
|
||||
- authentik.sources.ldap
|
||||
- authentik.sources.oauth
|
||||
- authentik.sources.plex
|
||||
- authentik.sources.saml
|
||||
- authentik.stages.authenticator_static
|
||||
- authentik.stages.authenticator_totp
|
||||
|
@ -17481,6 +17719,17 @@ definitions:
|
|||
enum:
|
||||
- all
|
||||
- any
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
UserSetting:
|
||||
required:
|
||||
- object_uid
|
||||
|
@ -17561,6 +17810,17 @@ definitions:
|
|||
enum:
|
||||
- all
|
||||
- any
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
server_uri:
|
||||
title: Server URI
|
||||
type: string
|
||||
|
@ -17741,6 +18001,17 @@ definitions:
|
|||
enum:
|
||||
- all
|
||||
- any
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
provider_type:
|
||||
title: Provider type
|
||||
type: string
|
||||
|
@ -17811,6 +18082,132 @@ definitions:
|
|||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
PlexSource:
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
title: Pbm uuid
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
name:
|
||||
title: Name
|
||||
description: Source's display Name.
|
||||
type: string
|
||||
minLength: 1
|
||||
slug:
|
||||
title: Slug
|
||||
description: Internal source name, used in URLs.
|
||||
type: string
|
||||
format: slug
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
maxLength: 50
|
||||
minLength: 1
|
||||
enabled:
|
||||
title: Enabled
|
||||
type: boolean
|
||||
authentication_flow:
|
||||
title: Authentication flow
|
||||
description: Flow to use when authenticating existing users.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
enrollment_flow:
|
||||
title: Enrollment flow
|
||||
description: Flow to use when enrolling new users.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
component:
|
||||
title: Component
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
policy_engine_mode:
|
||||
title: Policy engine mode
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
client_id:
|
||||
title: Client id
|
||||
description: Client identifier used to talk to Plex.
|
||||
type: string
|
||||
minLength: 1
|
||||
allowed_servers:
|
||||
description: Which servers a user has to be a member of to be granted access.
|
||||
Empty list allows every server.
|
||||
type: array
|
||||
items:
|
||||
title: Allowed servers
|
||||
type: string
|
||||
minLength: 1
|
||||
PlexTokenRedeem:
|
||||
required:
|
||||
- plex_token
|
||||
type: object
|
||||
properties:
|
||||
plex_token:
|
||||
title: Plex token
|
||||
type: string
|
||||
minLength: 1
|
||||
RedirectChallenge:
|
||||
required:
|
||||
- type
|
||||
- to
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
title: Type
|
||||
type: string
|
||||
enum:
|
||||
- native
|
||||
- shell
|
||||
- redirect
|
||||
component:
|
||||
title: Component
|
||||
type: string
|
||||
minLength: 1
|
||||
title:
|
||||
title: Title
|
||||
type: string
|
||||
minLength: 1
|
||||
background:
|
||||
title: Background
|
||||
type: string
|
||||
minLength: 1
|
||||
response_errors:
|
||||
title: Response errors
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ErrorDetail'
|
||||
to:
|
||||
title: To
|
||||
type: string
|
||||
minLength: 1
|
||||
SAMLSource:
|
||||
required:
|
||||
- name
|
||||
|
@ -17870,6 +18267,17 @@ definitions:
|
|||
enum:
|
||||
- all
|
||||
- any
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
pre_authentication_flow:
|
||||
title: Pre authentication flow
|
||||
description: Flow used before authentication.
|
||||
|
@ -18615,6 +19023,17 @@ definitions:
|
|||
enabled:
|
||||
title: Enabled
|
||||
type: boolean
|
||||
user_matching_mode:
|
||||
title: User matching mode
|
||||
description: How the source determines if an existing user should
|
||||
be authenticated or a new user enrolled.
|
||||
type: string
|
||||
enum:
|
||||
- identifier
|
||||
- email_link
|
||||
- email_deny
|
||||
- username_link
|
||||
- username_deny
|
||||
authentication_flow:
|
||||
title: Authentication flow
|
||||
description: Flow to use when authenticating existing users.
|
||||
|
@ -18673,6 +19092,10 @@ definitions:
|
|||
x-nullable: true
|
||||
readOnly: true
|
||||
readOnly: true
|
||||
single_use:
|
||||
title: Single use
|
||||
description: When enabled, the invitation will be deleted after usage.
|
||||
type: boolean
|
||||
InvitationStage:
|
||||
required:
|
||||
- name
|
||||
|
|
|
@ -147,11 +147,11 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
|
@ -206,11 +206,11 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
|
@ -245,11 +245,11 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
|
@ -338,17 +338,18 @@ class TestSourceOAuth1(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||
self.driver.find_element(By.NAME, "username").send_keys("example-user")
|
||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||
sleep(2)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
|
|
|
@ -140,11 +140,11 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
|
@ -208,11 +208,11 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
sleep(1)
|
||||
|
||||
|
@ -289,11 +289,11 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
|
|
|
@ -32,7 +32,7 @@ from authentik.core.api.users import UserSerializer
|
|||
from authentik.core.models import User
|
||||
from authentik.managed.manager import ObjectManager
|
||||
|
||||
RETRIES = int(environ.get("RETRIES", "3"))
|
||||
RETRIES = int(environ.get("RETRIES", "5"))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def USER() -> User: # noqa
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"@babel/typescript"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:lit/recommended"
|
||||
"plugin:lit/recommended",
|
||||
"plugin:custom-elements/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
|
@ -15,7 +16,8 @@
|
|||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"lit"
|
||||
"lit",
|
||||
"custom-elements"
|
||||
],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
FROM node as npm-builder
|
||||
|
||||
COPY . /static/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i --production=false && npm run build
|
||||
|
||||
FROM nginx
|
||||
|
||||
RUN mkdir /usr/share/nginx/html/.well-known
|
||||
COPY --from=npm-builder /static/robots.txt /usr/share/nginx/html/robots.txt
|
||||
COPY --from=npm-builder /static/security.txt /usr/share/nginx/html/.well-known/security.txt
|
||||
COPY --from=npm-builder /static/dist/ /usr/share/nginx/html/static/dist/
|
||||
COPY --from=npm-builder /static/authentik/ /usr/share/nginx/html/static/authentik/
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
|
@ -3,12 +3,6 @@ trigger:
|
|||
- next
|
||||
- version-*
|
||||
|
||||
variables:
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], '/', '-') }}
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||
|
||||
stages:
|
||||
- stage: generate
|
||||
jobs:
|
||||
|
@ -18,7 +12,7 @@ stages:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
|
@ -37,7 +31,7 @@ stages:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
|
@ -59,7 +53,7 @@ stages:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
|
@ -83,7 +77,7 @@ stages:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
|
@ -99,27 +93,3 @@ stages:
|
|||
command: 'custom'
|
||||
workingDir: 'web/'
|
||||
customCommand: 'run build'
|
||||
- stage: build_docker
|
||||
jobs:
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'ts_swagger_client'
|
||||
path: "web/api/"
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
python ./scripts/az_do_set_branch.py
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'beryjuorg-harbor'
|
||||
repository: 'authentik/static'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'web/Dockerfile'
|
||||
tags: "gh-$(branchName)"
|
||||
buildContext: 'web/'
|
||||
|
|
555
web/package-lock.json
generated
555
web/package-lock.json
generated
|
@ -296,11 +296,11 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz",
|
||||
"integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.0",
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
|
@ -321,9 +321,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz",
|
||||
"integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q=="
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.0",
|
||||
|
@ -341,9 +341,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
|
||||
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
|
@ -378,24 +378,91 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz",
|
||||
"integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
|
||||
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-replace-supers": "^7.13.12",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.13.13",
|
||||
"@babel/types": "^7.13.14"
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw=="
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -731,9 +798,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.0.tgz",
|
||||
"integrity": "sha512-6pXDPguA5zC40Y8oI5mqr+jEUpjMJonKvknvA+vD8CYDz5uuXEwWBK8sRAsE/t3gfb1k15AQb9RhwpscC4nUJQ==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.1.tgz",
|
||||
"integrity": "sha512-r8rsUahG4ywm0QpGcCrLaUSOuNAISR3IZCg4Fx05Ozq31aCUrQsTLH6KPxy0N5ULoQ4Sn9qjNdGNtbPWAC6hYg==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.12.13",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
|
@ -917,9 +984,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-transform-block-scoping": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.13.16.tgz",
|
||||
"integrity": "sha512-ad3PHUxGnfWF4Efd3qFuznEtZKoBp0spS+DgqzVzRPV7urEBvPLue3y2j80w4Jf2YLzZHj8TOv/Lmvdmh3b2xg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.1.tgz",
|
||||
"integrity": "sha512-2mQXd0zBrwfp0O1moWIhPpEeTKDvxyHcnma3JATVP1l+CctWBuot6OJG8LQ4DnBj4ZZPSmlb/fm4mu47EOAnVA==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.13.0"
|
||||
}
|
||||
|
@ -1028,95 +1095,6 @@
|
|||
"@babel/helper-module-transforms": "^7.14.0",
|
||||
"@babel/helper-plugin-utils": "^7.13.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz",
|
||||
"integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
|
||||
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-replace-supers": "^7.13.12",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz",
|
||||
"integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
|
||||
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-modules-commonjs": {
|
||||
|
@ -1128,95 +1106,6 @@
|
|||
"@babel/helper-plugin-utils": "^7.13.0",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz",
|
||||
"integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
|
||||
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-replace-supers": "^7.13.12",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz",
|
||||
"integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
|
||||
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-modules-systemjs": {
|
||||
|
@ -1245,95 +1134,6 @@
|
|||
"requires": {
|
||||
"@babel/helper-module-transforms": "^7.14.0",
|
||||
"@babel/helper-plugin-utils": "^7.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz",
|
||||
"integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
|
||||
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-replace-supers": "^7.13.12",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz",
|
||||
"integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
|
||||
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": {
|
||||
|
@ -1524,9 +1324,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/preset-env": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.0.tgz",
|
||||
"integrity": "sha512-GWRCdBv2whxqqaSi7bo/BEXf070G/fWFMEdCnmoRg2CZJy4GK06ovFuEjJrZhDRXYgBsYtxVbG8GUHvw+UWBkQ==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.1.tgz",
|
||||
"integrity": "sha512-0M4yL1l7V4l+j/UHvxcdvNfLB9pPtIooHTbEhgD/6UGyh8Hy3Bm1Mj0buzjDXATCSz3JFibVdnoJZCrlUCanrQ==",
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.14.0",
|
||||
"@babel/helper-compilation-targets": "^7.13.16",
|
||||
|
@ -1565,7 +1365,7 @@
|
|||
"@babel/plugin-transform-arrow-functions": "^7.13.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.13.0",
|
||||
"@babel/plugin-transform-block-scoped-functions": "^7.12.13",
|
||||
"@babel/plugin-transform-block-scoping": "^7.13.16",
|
||||
"@babel/plugin-transform-block-scoping": "^7.14.1",
|
||||
"@babel/plugin-transform-classes": "^7.13.0",
|
||||
"@babel/plugin-transform-computed-properties": "^7.13.0",
|
||||
"@babel/plugin-transform-destructuring": "^7.13.17",
|
||||
|
@ -1595,7 +1395,7 @@
|
|||
"@babel/plugin-transform-unicode-escapes": "^7.12.13",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.12.13",
|
||||
"@babel/preset-modules": "^0.1.4",
|
||||
"@babel/types": "^7.14.0",
|
||||
"@babel/types": "^7.14.1",
|
||||
"babel-plugin-polyfill-corejs2": "^0.2.0",
|
||||
"babel-plugin-polyfill-corejs3": "^0.2.0",
|
||||
"babel-plugin-polyfill-regenerator": "^0.2.0",
|
||||
|
@ -1625,9 +1425,9 @@
|
|||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
|
||||
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
|
@ -2302,27 +2102,27 @@
|
|||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.4.tgz",
|
||||
"integrity": "sha512-AXqHK5aeMKJPc4zf4lLBlj9TOxzSAmht4Zk0TxXWCsJ6AFP2H/nq20przQJkFyc7m8Ob8VhiNkeA7BQsMyiX6g==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.5.tgz",
|
||||
"integrity": "sha512-fjkhPR5gLCGVWhbWjEoN64hnmTvfTLRCgWmYTc9SiGchWFoFEmLqZyF2uJFyt27+qamLQ9fN58nnv4Ly2yyxqg==",
|
||||
"requires": {
|
||||
"@sentry/core": "6.3.4",
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/utils": "6.3.4",
|
||||
"@sentry/core": "6.3.5",
|
||||
"@sentry/types": "6.3.5",
|
||||
"@sentry/utils": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz",
|
||||
"integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g=="
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz",
|
||||
"integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz",
|
||||
"integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz",
|
||||
"integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/types": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
|
@ -2334,48 +2134,48 @@
|
|||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.4.tgz",
|
||||
"integrity": "sha512-M1C09EFpRDYDU968dk4rDIciX7v4q2eewS9kBPGwdWLbuSIO9yhsvEw3bK1XqatQSxnfQoXsO33ADq/JdWnGUA==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.5.tgz",
|
||||
"integrity": "sha512-VR2ibDy33mryD0mT6d9fGhKjdNzS2FSwwZPe9GvmNOjkyjly/oV91BKVoYJneCqOeq8fyj2lvkJGKuupdJNDqg==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.3.4",
|
||||
"@sentry/minimal": "6.3.4",
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/utils": "6.3.4",
|
||||
"@sentry/hub": "6.3.5",
|
||||
"@sentry/minimal": "6.3.5",
|
||||
"@sentry/types": "6.3.5",
|
||||
"@sentry/utils": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/hub": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.4.tgz",
|
||||
"integrity": "sha512-G9JVP851ovzkOnNzzm9s+g6Fkl+2ulfgsdjE8afd6/y0xUliCScdUCyU408AxnSyKTmBb8Cx7J+FDiqM+HQeaA==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.5.tgz",
|
||||
"integrity": "sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/utils": "6.3.4",
|
||||
"@sentry/types": "6.3.5",
|
||||
"@sentry/utils": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.4.tgz",
|
||||
"integrity": "sha512-amtQXu6jQmBjBJJTyvzsvMWFmwP3kfdkj2LVfNA40qInr6IJ200jQrZ/KUKngScK0kPfNDW4qFTE/U4J60m22Q==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.5.tgz",
|
||||
"integrity": "sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.3.4",
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/hub": "6.3.5",
|
||||
"@sentry/types": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz",
|
||||
"integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g=="
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz",
|
||||
"integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz",
|
||||
"integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz",
|
||||
"integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/types": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
|
@ -2387,12 +2187,12 @@
|
|||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.4.tgz",
|
||||
"integrity": "sha512-G9JVP851ovzkOnNzzm9s+g6Fkl+2ulfgsdjE8afd6/y0xUliCScdUCyU408AxnSyKTmBb8Cx7J+FDiqM+HQeaA==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.5.tgz",
|
||||
"integrity": "sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/utils": "6.3.4",
|
||||
"@sentry/types": "6.3.5",
|
||||
"@sentry/utils": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -2404,12 +2204,12 @@
|
|||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.4.tgz",
|
||||
"integrity": "sha512-amtQXu6jQmBjBJJTyvzsvMWFmwP3kfdkj2LVfNA40qInr6IJ200jQrZ/KUKngScK0kPfNDW4qFTE/U4J60m22Q==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.5.tgz",
|
||||
"integrity": "sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.3.4",
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/hub": "6.3.5",
|
||||
"@sentry/types": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -2421,14 +2221,14 @@
|
|||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.3.4.tgz",
|
||||
"integrity": "sha512-CpjIfVpi/u/Uraz1mUsteytovn47aGLWltAFrpn7bew/Y0hqnULGx/D/FwtQ4EbcdgULNtOX+nTrxJ5abmwZ3w==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.3.5.tgz",
|
||||
"integrity": "sha512-TNKAST1ge2g24BlTfVxNp4gP5t3drbi0OVCh8h8ah+J7UjHSfdiqhd9W2h5qv1GO61gGlpWeN/TyioyQmOxu0Q==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.3.4",
|
||||
"@sentry/minimal": "6.3.4",
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/utils": "6.3.4",
|
||||
"@sentry/hub": "6.3.5",
|
||||
"@sentry/minimal": "6.3.5",
|
||||
"@sentry/types": "6.3.5",
|
||||
"@sentry/utils": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -2440,16 +2240,16 @@
|
|||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz",
|
||||
"integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g=="
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz",
|
||||
"integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz",
|
||||
"integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz",
|
||||
"integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.3.4",
|
||||
"@sentry/types": "6.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -3174,9 +2974,9 @@
|
|||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.2.0.tgz",
|
||||
"integrity": "sha512-Ml3R47TvOPW6gQ6T8mg/uPvyOASPpPVVF6xb7ZyHkek1c6kJIT5ScT559afXoDf6uwtpDR2BpCommkA5KT8ODg=="
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.2.1.tgz",
|
||||
"integrity": "sha512-XsNDf3854RGZkLCt+5vWAXGAtUdKP2nhfikLGZqud6G4CvRE2ts64TIxTTfspOin2kEZvPgomE29E6oU02dYjQ=="
|
||||
},
|
||||
"chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
|
@ -3723,6 +3523,14 @@
|
|||
"resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz",
|
||||
"integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw=="
|
||||
},
|
||||
"eslint-plugin-custom-elements": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-custom-elements/-/eslint-plugin-custom-elements-0.0.2.tgz",
|
||||
"integrity": "sha512-lIRBhxh0M/1seyMzSPJwdfdNtlVSPArJ+erF2xqjPsd/6SdCuT43hCQNV2A2te3GqBWhgh/unXSVRO09c1kyPA==",
|
||||
"requires": {
|
||||
"eslint-rule-documentation": ">=1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-lit": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz",
|
||||
|
@ -3733,6 +3541,11 @@
|
|||
"requireindex": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"eslint-rule-documentation": {
|
||||
"version": "1.0.23",
|
||||
"resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz",
|
||||
"integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw=="
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
|
@ -4924,9 +4737,9 @@
|
|||
}
|
||||
},
|
||||
"lit-element": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.4.0.tgz",
|
||||
"integrity": "sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.0.tgz",
|
||||
"integrity": "sha512-SS6Bmm7FYw/RVeD6YD3gAjrT0ss6rOQHaacUnDCyVE3sDuUpEmr+Gjl0QUHnD8+0mM5apBbnA60NkFJ2kqcOMA==",
|
||||
"requires": {
|
||||
"lit-html": "^1.1.1"
|
||||
}
|
||||
|
@ -5693,6 +5506,14 @@
|
|||
"prismjs": "^1.23.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit-element": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.4.0.tgz",
|
||||
"integrity": "sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==",
|
||||
"requires": {
|
||||
"lit-html": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"lit-html": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"@babel/core": "^7.14.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.13.15",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.14.0",
|
||||
"@babel/preset-env": "^7.14.1",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@lingui/cli": "^3.8.10",
|
||||
|
@ -50,8 +50,8 @@
|
|||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sentry/browser": "^6.3.4",
|
||||
"@sentry/tracing": "^6.3.4",
|
||||
"@sentry/browser": "^6.3.5",
|
||||
"@sentry/tracing": "^6.3.5",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/grecaptcha": "^3.0.2",
|
||||
|
@ -61,15 +61,16 @@
|
|||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.2.0",
|
||||
"chart.js": "^3.2.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.61.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.2",
|
||||
"eslint-plugin-lit": "^1.3.0",
|
||||
"flowchart.js": "^1.15.0",
|
||||
"lit-element": "^2.4.0",
|
||||
"lit-element": "^2.5.0",
|
||||
"lit-html": "^1.4.0",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
|
|
|
@ -272,7 +272,7 @@ body {
|
|||
.pf-c-login__main-header-desc {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-login__main-footer-links-item-link > img {
|
||||
.pf-c-login__main-footer-links-item img {
|
||||
filter: invert(1);
|
||||
}
|
||||
.pf-c-login__main-footer-band {
|
||||
|
|
|
@ -54,7 +54,7 @@ export class Tabs extends LitElement {
|
|||
this.currentPage = slot;
|
||||
const currentUrl = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||
const newUrl = `#${currentUrl};${slot}`;
|
||||
window.location.hash = newUrl;
|
||||
history.replaceState(undefined, "", newUrl);
|
||||
}
|
||||
|
||||
renderTab(page: Element): TemplateResult {
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import AKGlobal from "../../authentik.css";
|
||||
|
||||
import { configureSentry } from "../../api/Sentry";
|
||||
import { Config } from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { EVENT_SIDEBAR_TOGGLE } from "../../constants";
|
||||
|
||||
// If the viewport is wider than MIN_WIDTH, the sidebar
|
||||
// is shown besides the content, and not overlayed.
|
||||
export const MIN_WIDTH = 1200;
|
||||
|
||||
export const DefaultConfig: Config = {
|
||||
brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg",
|
||||
|
@ -21,12 +30,15 @@ export class SidebarBrand extends LitElement {
|
|||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFGlobal,
|
||||
PFPage,
|
||||
PFButton,
|
||||
AKGlobal,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 114px;
|
||||
min-height: 114px;
|
||||
|
@ -36,19 +48,47 @@ export class SidebarBrand extends LitElement {
|
|||
padding: 0 .5rem;
|
||||
height: 42px;
|
||||
}
|
||||
button.pf-c-button.sidebar-trigger {
|
||||
background-color: transparent;
|
||||
border-radius: 0px;
|
||||
height: 100%;
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
window.addEventListener("resize", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
configureSentry(true).then((c) => {this.config = c;});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <a href="#/" class="pf-c-page__header-brand-link">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="${ifDefined(this.config.brandingLogo)}" alt="authentik icon" loading="lazy" />
|
||||
</div>
|
||||
</a>`;
|
||||
return html`
|
||||
${window.innerWidth <= MIN_WIDTH ? html`
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
` : html``}
|
||||
<a href="#/" class="pf-c-page__header-brand-link">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="${ifDefined(this.config.brandingLogo)}" alt="authentik icon" loading="lazy" />
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue