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:
Jens Langhammer 2021-05-04 20:57:48 +02:00
commit 99d161e212
127 changed files with 4765 additions and 6317 deletions

View file

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

View file

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

View file

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

View file

@ -59,3 +59,4 @@ pylint-django = "*"
pytest = "*"
pytest-django = "*"
selenium = "*"
requests-mock = "*"

76
Pipfile.lock generated
View file

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

View file

@ -3,7 +3,7 @@
{% load static %}
{% block title %}
authentik API Browser
API Browser - {{ config.authentik.branding.title }}
{% endblock %}
{% block head %}

View file

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

View file

@ -45,6 +45,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"verbose_name",
"verbose_name_plural",
"policy_engine_mode",
"user_matching_mode",
]

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
{% load i18n %}
{% block title %}
{% trans 'Permission denied - authentik' %}
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }}
{% endblock %}
{% block card_title %}

View file

@ -14,7 +14,7 @@
{% endblock %}
{% block title %}
{% trans 'End session' %}
{% trans 'End session' %} - {{ config.authentik.branding.title }}
{% endblock %}
{% block card_title %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View 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")

View 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

View 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())

View file

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

View file

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

View file

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

View file

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

View file

@ -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/",
}
],
},

View file

@ -39,6 +39,7 @@ class InvitationSerializer(ModelSerializer):
"expires",
"fixed_data",
"created_by",
"single_use",
]
depth = 2

View file

@ -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.",
),
),
]

View file

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

View file

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

View file

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

View file

@ -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
View 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()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -79,7 +79,10 @@ spec:
{{- end }}
ports:
- name: http
containerPort: 8000
containerPort: 9000
protocol: TCP
- name: https
containerPort: 9443
protocol: TCP
livenessProbe:
httpGet:

View file

@ -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
View 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
View 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"`
}

View file

@ -0,0 +1,3 @@
package constants
const VERSION = "2021.4.5"

View 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)
}

View 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()
}

View 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()
})
}

View 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
View 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
View 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
View 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())
}

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
package main
func main() {
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
"@babel/typescript"
],
"plugins": [
["@babel/plugin-proposal-private-methods", { "loose": true }],
[
"@babel/plugin-proposal-decorators",
{

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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