Merge branch 'master' into inbuilt-proxy
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> # Conflicts: # internal/constants/constants.go # outpost/pkg/version.go
This commit is contained in:
commit
948db46406
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.6.3
|
current_version = 2021.6.4
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
|
|
|
@ -33,14 +33,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2021.6.3,
|
beryju/authentik:2021.6.4,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.6.3,
|
ghcr.io/goauthentik/server:2021.6.4,
|
||||||
ghcr.io/goauthentik/server:latest
|
ghcr.io/goauthentik/server:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik:latest
|
docker pull beryju/authentik:latest
|
||||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||||
|
@ -75,14 +75,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.6.3,
|
beryju/authentik-proxy:2021.6.4,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-proxy:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.6.3,
|
ghcr.io/goauthentik/proxy:2021.6.4,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/proxy:latest
|
||||||
file: proxy.Dockerfile
|
file: proxy.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-proxy:latest
|
docker pull beryju/authentik-proxy:latest
|
||||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||||
|
@ -117,14 +117,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-ldap:2021.6.3,
|
beryju/authentik-ldap:2021.6.4,
|
||||||
beryju/authentik-ldap:latest,
|
beryju/authentik-ldap:latest,
|
||||||
ghcr.io/goauthentik/ldap:2021.6.3,
|
ghcr.io/goauthentik/ldap:2021.6.4,
|
||||||
ghcr.io/goauthentik/ldap:latest
|
ghcr.io/goauthentik/ldap:latest
|
||||||
file: ldap.Dockerfile
|
file: ldap.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-ldap:latest
|
docker pull beryju/authentik-ldap:latest
|
||||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||||
|
@ -176,7 +176,6 @@ jobs:
|
||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.6.3
|
version: authentik@2021.6.4
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
finalize: false
|
|
||||||
|
|
|
@ -122,19 +122,19 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:055f9dc07f95f202a4dc25196a3a9f1e2f137171ee364cf980e4673de75fb529",
|
"sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03",
|
||||||
"sha256:bc9b278e362ec9b531511a498262297f074c4f5ca9560455919a0af1a4698615"
|
"sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.17.104"
|
"version": "==1.17.105"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:23aa3238c004319f78423eb8cbba2813b62ee64d0e3bab04e0a00e067f99542a",
|
"sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c",
|
||||||
"sha256:95ab472c8254b8d2cfa6d719b433e511fbcf80895b4cd18e4219c1efa0b78270"
|
"sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||||
"version": "==1.20.104"
|
"version": "==1.20.105"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -778,11 +778,11 @@
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==20.9"
|
"version": "==21.0"
|
||||||
},
|
},
|
||||||
"prometheus-client": {
|
"prometheus-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1585,7 +1585,7 @@
|
||||||
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
|
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
|
||||||
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
|
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
|
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
||||||
"version": "==5.9.1"
|
"version": "==5.9.1"
|
||||||
},
|
},
|
||||||
"lazy-object-proxy": {
|
"lazy-object-proxy": {
|
||||||
|
@ -1632,11 +1632,11 @@
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==20.9"
|
"version": "==21.0"
|
||||||
},
|
},
|
||||||
"pathspec": {
|
"pathspec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.6.3"
|
__version__ = "2021.6.4"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http.response import Http404
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django_filters.filters import BooleanFilter, CharFilter
|
from django_filters.filters import BooleanFilter, CharFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
|
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||||
|
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
"200": LinkSerializer(many=False),
|
"200": LinkSerializer(many=False),
|
||||||
"404": OpenApiResponse(description="No recovery flow found."),
|
"404": LinkSerializer(many=False),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||||
|
@ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
# Check that there is a recovery flow, if not return an error
|
# Check that there is a recovery flow, if not return an error
|
||||||
flow = tenant.flow_recovery
|
flow = tenant.flow_recovery
|
||||||
if not flow:
|
if not flow:
|
||||||
raise Http404
|
return Response({"link": ""}, status=404)
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
token, __ = Token.objects.get_or_create(
|
token, __ = Token.objects.get_or_create(
|
||||||
identifier=f"{user.uid}-password-reset",
|
identifier=f"{user.uid}-password-reset",
|
||||||
|
|
|
@ -14,7 +14,9 @@ def is_dict(value: Any):
|
||||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return
|
return
|
||||||
raise ValidationError("Value must be a dictionary.")
|
raise ValidationError(
|
||||||
|
"Value must be a dictionary, and not have any duplicate keys."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PassiveSerializer(Serializer):
|
class PassiveSerializer(Serializer):
|
||||||
|
|
|
@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"fingerprint",
|
"fingerprint_sha256",
|
||||||
|
"fingerprint_sha1",
|
||||||
"certificate_data",
|
"certificate_data",
|
||||||
"key_data",
|
"key_data",
|
||||||
"cert_expiry",
|
"cert_expiry",
|
||||||
|
|
|
@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||||
return self._private_key
|
return self._private_key
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fingerprint(self) -> str:
|
def fingerprint_sha256(self) -> str:
|
||||||
"""Get SHA256 Fingerprint of certificate_data"""
|
"""Get SHA256 Fingerprint of certificate_data"""
|
||||||
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
||||||
"utf-8"
|
"utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fingerprint_sha1(self) -> str:
|
||||||
|
"""Get SHA1 Fingerprint of certificate_data"""
|
||||||
|
return hexlify(
|
||||||
|
self.certificate.fingerprint(hashes.SHA1()), ":" # nosec
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kid(self):
|
def kid(self):
|
||||||
"""Get Key ID used for JWKS"""
|
"""Get Key ID used for JWKS"""
|
||||||
|
|
|
@ -3,6 +3,7 @@ from functools import partial
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
@ -63,7 +64,15 @@ class AuditMiddleware:
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
return
|
return
|
||||||
if before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
# Special case for SuspiciousOperation, we have a special event action for that
|
||||||
|
if isinstance(exception, SuspiciousOperation):
|
||||||
|
thread = EventNewThread(
|
||||||
|
EventAction.SUSPICIOUS_REQUEST,
|
||||||
|
request,
|
||||||
|
message=str(exception),
|
||||||
|
)
|
||||||
|
thread.run()
|
||||||
|
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||||
thread = EventNewThread(
|
thread = EventNewThread(
|
||||||
EventAction.SYSTEM_EXCEPTION,
|
EventAction.SYSTEM_EXCEPTION,
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="invalid_response_action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("retry", "Retry"),
|
||||||
|
("restart", "Restart"),
|
||||||
|
("restart_with_context", "Restart With Context"),
|
||||||
|
],
|
||||||
|
default="retry",
|
||||||
|
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -134,7 +134,7 @@ class FlowExecutorView(APIView):
|
||||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||||
return self.stage_invalid(error_message=message)
|
return self.stage_invalid(error_message=message)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument, too-many-return-statements
|
||||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||||
# Early check if theres an active Plan for the current session
|
# Early check if theres an active Plan for the current session
|
||||||
if SESSION_KEY_PLAN in self.request.session:
|
if SESSION_KEY_PLAN in self.request.session:
|
||||||
|
@ -167,7 +167,18 @@ class FlowExecutorView(APIView):
|
||||||
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
|
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
|
||||||
# We don't save the Plan after getting the next stage
|
# We don't save the Plan after getting the next stage
|
||||||
# as it hasn't been successfully passed yet
|
# as it hasn't been successfully passed yet
|
||||||
next_binding = self.plan.next(self.request)
|
try:
|
||||||
|
# This is the first time we actually access any attribute on the selected plan
|
||||||
|
# if the cached plan is from an older version, it might have different attributes
|
||||||
|
# in which case we just delete the plan and invalidate everything
|
||||||
|
next_binding = self.plan.next(self.request)
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
self._logger.warning(
|
||||||
|
"f(exec): found incompatible flow plan, invalidating run", exc=exc
|
||||||
|
)
|
||||||
|
keys = cache.keys("flow_*")
|
||||||
|
cache.delete_many(keys)
|
||||||
|
return self.stage_invalid()
|
||||||
if not next_binding:
|
if not next_binding:
|
||||||
self._logger.debug("f(exec): no more stages, flow is done.")
|
self._logger.debug("f(exec): no more stages, flow is done.")
|
||||||
return self._flow_done()
|
return self._flow_done()
|
||||||
|
|
|
@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
(
|
(
|
||||||
f"Outpost type {self.initial_data['type']} can't be used with "
|
f"Outpost type {self.initial_data['type']} can't be used with "
|
||||||
f"{type(provider)} providers."
|
f"{provider.__class__.__name__} providers."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return providers
|
return providers
|
||||||
|
|
|
@ -36,8 +36,10 @@ class DockerController(BaseController):
|
||||||
|
|
||||||
def _get_env(self) -> dict[str, str]:
|
def _get_env(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
||||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
"AUTHENTIK_INSECURE": str(
|
||||||
|
self.outpost.config.authentik_host_insecure
|
||||||
|
).lower(),
|
||||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,11 +47,10 @@ class DockerController(BaseController):
|
||||||
"""Check if container's env is equal to what we would set. Return true if container needs
|
"""Check if container's env is equal to what we would set. Return true if container needs
|
||||||
to be rebuilt."""
|
to be rebuilt."""
|
||||||
should_be = self._get_env()
|
should_be = self._get_env()
|
||||||
container_env = container.attrs.get("Config", {}).get("Env", {})
|
container_env = container.attrs.get("Config", {}).get("Env", [])
|
||||||
for key, expected_value in should_be.items():
|
for key, expected_value in should_be.items():
|
||||||
if key not in container_env:
|
entry = f"{key.upper()}={expected_value}"
|
||||||
continue
|
if entry not in container_env:
|
||||||
if container_env[key] != expected_value:
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -62,14 +63,17 @@ class DockerController(BaseController):
|
||||||
# When the container isn't running, the API doesn't report any port mappings
|
# When the container isn't running, the API doesn't report any port mappings
|
||||||
if container.status != "running":
|
if container.status != "running":
|
||||||
return False
|
return False
|
||||||
# {'6379/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '6379'}]}
|
# {'3389/tcp': [
|
||||||
|
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
|
||||||
|
# {'HostIp': '::', 'HostPort': '389'}
|
||||||
|
# ]}
|
||||||
for port in self.deployment_ports:
|
for port in self.deployment_ports:
|
||||||
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
||||||
if key not in container.ports:
|
if key not in container.ports:
|
||||||
return True
|
return True
|
||||||
host_matching = False
|
host_matching = False
|
||||||
for host_port in container.ports[key]:
|
for host_port in container.ports[key]:
|
||||||
host_matching = host_port.get("HostPort") == port.port
|
host_matching = host_port.get("HostPort") == str(port.port)
|
||||||
if not host_matching:
|
if not host_matching:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -79,7 +83,7 @@ class DockerController(BaseController):
|
||||||
try:
|
try:
|
||||||
return self.client.containers.get(container_name), False
|
return self.client.containers.get(container_name), False
|
||||||
except NotFound:
|
except NotFound:
|
||||||
self.logger.info("Container does not exist, creating")
|
self.logger.info("(Re-)creating container...")
|
||||||
image_name = self.get_container_image()
|
image_name = self.get_container_image()
|
||||||
self.client.images.pull(image_name)
|
self.client.images.pull(image_name)
|
||||||
container_args = {
|
container_args = {
|
||||||
|
@ -107,6 +111,7 @@ class DockerController(BaseController):
|
||||||
try:
|
try:
|
||||||
container, has_been_created = self._get_container()
|
container, has_been_created = self._get_container()
|
||||||
if has_been_created:
|
if has_been_created:
|
||||||
|
container.start()
|
||||||
return None
|
return None
|
||||||
# Check if the container is out of date, delete it and retry
|
# Check if the container is out of date, delete it and retry
|
||||||
if len(container.image.tags) > 0:
|
if len(container.image.tags) > 0:
|
||||||
|
@ -164,6 +169,7 @@ class DockerController(BaseController):
|
||||||
self.logger.info("Container is not running, restarting...")
|
self.logger.info("Container is not running, restarting...")
|
||||||
container.start()
|
container.start()
|
||||||
return None
|
return None
|
||||||
|
self.logger.info("Container is running")
|
||||||
return None
|
return None
|
||||||
except DockerException as exc:
|
except DockerException as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException(str(exc)) from exc
|
||||||
|
|
|
@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
|
||||||
},
|
},
|
||||||
"outposts_service_connection_check": {
|
"outposts_service_connection_check": {
|
||||||
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
|
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
|
||||||
"schedule": crontab(minute="*/60"),
|
"schedule": crontab(minute="*/5"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
"outpost_token_ensurer": {
|
"outpost_token_ensurer": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""authentik outpost signals"""
|
"""authentik outpost signals"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
@ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_):
|
||||||
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=Outpost.providers.through)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def m2m_changed_update(sender, instance: Model, action: str, **_):
|
||||||
|
"""Update outpost on m2m change, when providers are added or removed"""
|
||||||
|
if action in ["post_add", "post_remove", "post_clear"]:
|
||||||
|
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post_save_update(sender, instance: Model, **_):
|
def post_save_update(sender, instance: Model, **_):
|
||||||
|
|
|
@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
||||||
"expires",
|
"expires",
|
||||||
"scope",
|
"scope",
|
||||||
"id_token",
|
"id_token",
|
||||||
|
"revoked",
|
||||||
]
|
]
|
||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="authorizationcode",
|
||||||
|
name="revoked",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="refreshtoken",
|
||||||
|
name="revoked",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -318,6 +318,7 @@ class BaseGrantModel(models.Model):
|
||||||
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
|
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||||
|
revoked = models.BooleanField(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scope(self) -> list[str]:
|
def scope(self) -> list[str]:
|
||||||
|
@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
||||||
# Convert datetimes into timestamps.
|
# Convert datetimes into timestamps.
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
iat_time = now
|
iat_time = now
|
||||||
exp_time = int(
|
exp_time = int(dateformat.format(self.expires, "U"))
|
||||||
now + timedelta_from_string(self.provider.token_validity).total_seconds()
|
|
||||||
)
|
|
||||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||||
auth_events = Event.objects.filter(
|
auth_events = Event.objects.filter(
|
||||||
action=EventAction.LOGIN, user=get_user(user)
|
action=EventAction.LOGIN, user=get_user(user)
|
||||||
|
|
|
@ -6,6 +6,8 @@ from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||||
|
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
|
||||||
client_id=generate_client_id(),
|
client_id=generate_client_id(),
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_client_secret(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://testserver",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
)
|
)
|
||||||
header = b64encode(
|
header = b64encode(
|
||||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
|
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
|
||||||
data={
|
data={
|
||||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||||
"code": code.code,
|
"code": code.code,
|
||||||
"redirect_uri": "http://local.invalid",
|
"redirect_uri": "http://testserver",
|
||||||
},
|
},
|
||||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||||
)
|
)
|
||||||
params = TokenParams.from_request(request)
|
params = TokenParams.parse(
|
||||||
|
request, provider, provider.client_id, provider.client_secret
|
||||||
|
)
|
||||||
self.assertEqual(params.provider, provider)
|
self.assertEqual(params.provider, provider)
|
||||||
|
|
||||||
def test_request_refresh_token(self):
|
def test_request_refresh_token(self):
|
||||||
|
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_client_secret(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
)
|
)
|
||||||
header = b64encode(
|
header = b64encode(
|
||||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
|
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
|
||||||
},
|
},
|
||||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||||
)
|
)
|
||||||
params = TokenParams.from_request(request)
|
params = TokenParams.parse(
|
||||||
|
request, provider, provider.client_id, provider.client_secret
|
||||||
|
)
|
||||||
self.assertEqual(params.provider, provider)
|
self.assertEqual(params.provider, provider)
|
||||||
|
|
||||||
def test_auth_code_view(self):
|
def test_auth_code_view(self):
|
||||||
|
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_client_secret(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
# Needs to be assigned to an application for iss to be set
|
||||||
self.app.provider = provider
|
self.app.provider = provider
|
||||||
|
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_client_secret(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
# Needs to be assigned to an application for iss to be set
|
||||||
self.app.provider = provider
|
self.app.provider = provider
|
||||||
|
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_client_secret(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
)
|
)
|
||||||
header = b64encode(
|
header = b64encode(
|
||||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
|
@ -230,3 +241,65 @@ class TestToken(OAuthTestCase):
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_refresh_token_revoke(self):
|
||||||
|
"""test request param"""
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="test",
|
||||||
|
client_id=generate_client_id(),
|
||||||
|
client_secret=generate_client_secret(),
|
||||||
|
authorization_flow=Flow.objects.first(),
|
||||||
|
redirect_uris="http://testserver",
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
)
|
||||||
|
# Needs to be assigned to an application for iss to be set
|
||||||
|
self.app.provider = provider
|
||||||
|
self.app.save()
|
||||||
|
header = b64encode(
|
||||||
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
|
).decode()
|
||||||
|
user = User.objects.get(username="akadmin")
|
||||||
|
token: RefreshToken = RefreshToken.objects.create(
|
||||||
|
provider=provider,
|
||||||
|
user=user,
|
||||||
|
refresh_token=generate_client_id(),
|
||||||
|
)
|
||||||
|
# Create initial refresh token
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
data={
|
||||||
|
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||||
|
"refresh_token": token.refresh_token,
|
||||||
|
"redirect_uri": "http://testserver",
|
||||||
|
},
|
||||||
|
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||||
|
)
|
||||||
|
new_token: RefreshToken = (
|
||||||
|
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
|
||||||
|
)
|
||||||
|
# Post again with initial token -> get new refresh token
|
||||||
|
# and revoke old one
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
data={
|
||||||
|
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||||
|
"refresh_token": new_token.refresh_token,
|
||||||
|
"redirect_uri": "http://local.invalid",
|
||||||
|
},
|
||||||
|
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
# Post again with old token, is now revoked and should error
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
data={
|
||||||
|
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||||
|
"refresh_token": new_token.refresh_token,
|
||||||
|
"redirect_uri": "http://local.invalid",
|
||||||
|
},
|
||||||
|
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists()
|
||||||
|
)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect
|
||||||
from django.utils.cache import patch_vary_headers
|
from django.utils.cache import patch_vary_headers
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.providers.oauth2.errors import BearerTokenError
|
from authentik.providers.oauth2.errors import BearerTokenError
|
||||||
from authentik.providers.oauth2.models import RefreshToken
|
from authentik.providers.oauth2.models import RefreshToken
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
|
||||||
if not allowed:
|
if not allowed:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"CORS: Origin is not an allowed origin",
|
"CORS: Origin is not an allowed origin",
|
||||||
requested=origin,
|
requested=received_origin,
|
||||||
allowed=allowed_origins,
|
allowed=allowed_origins,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
|
||||||
raise BearerTokenError("invalid_token")
|
raise BearerTokenError("invalid_token")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs["token"] = RefreshToken.objects.get(
|
token: RefreshToken = RefreshToken.objects.get(
|
||||||
access_token=access_token
|
access_token=access_token
|
||||||
)
|
)
|
||||||
except RefreshToken.DoesNotExist:
|
except RefreshToken.DoesNotExist:
|
||||||
LOGGER.debug("Token does not exist", access_token=access_token)
|
LOGGER.debug("Token does not exist", access_token=access_token)
|
||||||
raise BearerTokenError("invalid_token")
|
raise BearerTokenError("invalid_token")
|
||||||
|
|
||||||
if kwargs["token"].is_expired:
|
if token.is_expired:
|
||||||
LOGGER.debug("Token has expired", access_token=access_token)
|
LOGGER.debug("Token has expired", access_token=access_token)
|
||||||
raise BearerTokenError("invalid_token")
|
raise BearerTokenError("invalid_token")
|
||||||
|
|
||||||
if not set(scopes).issubset(set(kwargs["token"].scope)):
|
if token.revoked:
|
||||||
|
LOGGER.warning("Revoked token was used", access_token=access_token)
|
||||||
|
Event.new(
|
||||||
|
action=EventAction.SUSPICIOUS_REQUEST,
|
||||||
|
message="Revoked refresh token was used",
|
||||||
|
token=access_token,
|
||||||
|
).from_http(request)
|
||||||
|
raise BearerTokenError("invalid_token")
|
||||||
|
|
||||||
|
if not set(scopes).issubset(set(token.scope)):
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Scope missmatch.",
|
"Scope missmatch.",
|
||||||
required=set(scopes),
|
required=set(scopes),
|
||||||
token_has=set(kwargs["token"].scope),
|
token_has=set(token.scope),
|
||||||
)
|
)
|
||||||
raise BearerTokenError("insufficient_scope")
|
raise BearerTokenError("insufficient_scope")
|
||||||
except BearerTokenError as error:
|
except BearerTokenError as error:
|
||||||
|
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
|
||||||
"WWW-Authenticate"
|
"WWW-Authenticate"
|
||||||
] = f'error="{error.code}", error_description="{error.description}"'
|
] = f'error="{error.code}", error_description="{error.description}"'
|
||||||
return response
|
return response
|
||||||
|
kwargs["token"] = token
|
||||||
return view(request, *args, **kwargs)
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
return view_wrapper
|
return view_wrapper
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||||
|
@ -30,6 +31,7 @@ LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
class TokenParams:
|
class TokenParams:
|
||||||
"""Token params"""
|
"""Token params"""
|
||||||
|
|
||||||
|
@ -40,6 +42,8 @@ class TokenParams:
|
||||||
state: str
|
state: str
|
||||||
scope: list[str]
|
scope: list[str]
|
||||||
|
|
||||||
|
provider: OAuth2Provider
|
||||||
|
|
||||||
authorization_code: Optional[AuthorizationCode] = None
|
authorization_code: Optional[AuthorizationCode] = None
|
||||||
refresh_token: Optional[RefreshToken] = None
|
refresh_token: Optional[RefreshToken] = None
|
||||||
|
|
||||||
|
@ -47,35 +51,34 @@ class TokenParams:
|
||||||
|
|
||||||
raw_code: InitVar[str] = ""
|
raw_code: InitVar[str] = ""
|
||||||
raw_token: InitVar[str] = ""
|
raw_token: InitVar[str] = ""
|
||||||
|
request: InitVar[Optional[HttpRequest]] = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_request(request: HttpRequest) -> "TokenParams":
|
def parse(
|
||||||
"""Extract Token Parameters from http request"""
|
request: HttpRequest,
|
||||||
client_id, client_secret = extract_client_auth(request)
|
provider: OAuth2Provider,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
) -> "TokenParams":
|
||||||
|
"""Parse params for request"""
|
||||||
return TokenParams(
|
return TokenParams(
|
||||||
|
# Init vars
|
||||||
|
raw_code=request.POST.get("code", ""),
|
||||||
|
raw_token=request.POST.get("refresh_token", ""),
|
||||||
|
request=request,
|
||||||
|
# Regular params
|
||||||
|
provider=provider,
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
client_secret=client_secret,
|
client_secret=client_secret,
|
||||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||||
grant_type=request.POST.get("grant_type", ""),
|
grant_type=request.POST.get("grant_type", ""),
|
||||||
raw_code=request.POST.get("code", ""),
|
|
||||||
raw_token=request.POST.get("refresh_token", ""),
|
|
||||||
state=request.POST.get("state", ""),
|
state=request.POST.get("state", ""),
|
||||||
scope=request.POST.get("scope", "").split(),
|
scope=request.POST.get("scope", "").split(),
|
||||||
# PKCE parameter.
|
# PKCE parameter.
|
||||||
code_verifier=request.POST.get("code_verifier"),
|
code_verifier=request.POST.get("code_verifier"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __post_init__(self, raw_code, raw_token):
|
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
|
||||||
try:
|
|
||||||
provider: OAuth2Provider = OAuth2Provider.objects.get(
|
|
||||||
client_id=self.client_id
|
|
||||||
)
|
|
||||||
self.provider = provider
|
|
||||||
except OAuth2Provider.DoesNotExist:
|
|
||||||
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
|
|
||||||
raise TokenError("invalid_client")
|
|
||||||
|
|
||||||
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
||||||
if self.provider.client_secret != self.client_secret:
|
if self.provider.client_secret != self.client_secret:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
|
@ -87,7 +90,6 @@ class TokenParams:
|
||||||
|
|
||||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||||
self.__post_init_code(raw_code)
|
self.__post_init_code(raw_code)
|
||||||
|
|
||||||
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||||
if not raw_token:
|
if not raw_token:
|
||||||
LOGGER.warning("Missing refresh token")
|
LOGGER.warning("Missing refresh token")
|
||||||
|
@ -107,7 +109,14 @@ class TokenParams:
|
||||||
token=raw_token,
|
token=raw_token,
|
||||||
)
|
)
|
||||||
raise TokenError("invalid_grant")
|
raise TokenError("invalid_grant")
|
||||||
|
if self.refresh_token.revoked:
|
||||||
|
LOGGER.warning("Refresh token is revoked", token=raw_token)
|
||||||
|
Event.new(
|
||||||
|
action=EventAction.SUSPICIOUS_REQUEST,
|
||||||
|
message="Revoked refresh token was used",
|
||||||
|
token=raw_token,
|
||||||
|
).from_http(request)
|
||||||
|
raise TokenError("invalid_grant")
|
||||||
else:
|
else:
|
||||||
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
||||||
raise TokenError("unsupported_grant_type")
|
raise TokenError("unsupported_grant_type")
|
||||||
|
@ -159,13 +168,14 @@ class TokenParams:
|
||||||
class TokenView(View):
|
class TokenView(View):
|
||||||
"""Generate tokens for clients"""
|
"""Generate tokens for clients"""
|
||||||
|
|
||||||
|
provider: Optional[OAuth2Provider] = None
|
||||||
params: Optional[TokenParams] = None
|
params: Optional[TokenParams] = None
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
if self.params:
|
if self.provider:
|
||||||
allowed_origins = self.params.provider.redirect_uris.split("\n")
|
allowed_origins = self.provider.redirect_uris.split("\n")
|
||||||
cors_allow(self.request, response, *allowed_origins)
|
cors_allow(self.request, response, *allowed_origins)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -175,19 +185,32 @@ class TokenView(View):
|
||||||
def post(self, request: HttpRequest) -> HttpResponse:
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Generate tokens for clients"""
|
"""Generate tokens for clients"""
|
||||||
try:
|
try:
|
||||||
self.params = TokenParams.from_request(request)
|
client_id, client_secret = extract_client_auth(request)
|
||||||
|
try:
|
||||||
|
self.provider = OAuth2Provider.objects.get(client_id=client_id)
|
||||||
|
except OAuth2Provider.DoesNotExist:
|
||||||
|
LOGGER.warning(
|
||||||
|
"OAuth2Provider does not exist", client_id=self.client_id
|
||||||
|
)
|
||||||
|
raise TokenError("invalid_client")
|
||||||
|
|
||||||
|
if not self.provider:
|
||||||
|
raise ValueError
|
||||||
|
self.params = TokenParams.parse(
|
||||||
|
request, self.provider, client_id, client_secret
|
||||||
|
)
|
||||||
|
|
||||||
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||||
return TokenResponse(self.create_code_response_dic())
|
return TokenResponse(self.create_code_response())
|
||||||
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||||
return TokenResponse(self.create_refresh_response_dic())
|
return TokenResponse(self.create_refresh_response())
|
||||||
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
|
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
|
||||||
except TokenError as error:
|
except TokenError as error:
|
||||||
return TokenResponse(error.create_dict(), status=400)
|
return TokenResponse(error.create_dict(), status=400)
|
||||||
except UserAuthError as error:
|
except UserAuthError as error:
|
||||||
return TokenResponse(error.create_dict(), status=403)
|
return TokenResponse(error.create_dict(), status=403)
|
||||||
|
|
||||||
def create_code_response_dic(self) -> dict[str, Any]:
|
def create_code_response(self) -> dict[str, Any]:
|
||||||
"""See https://tools.ietf.org/html/rfc6749#section-4.1"""
|
"""See https://tools.ietf.org/html/rfc6749#section-4.1"""
|
||||||
|
|
||||||
refresh_token = self.params.authorization_code.provider.create_refresh_token(
|
refresh_token = self.params.authorization_code.provider.create_refresh_token(
|
||||||
|
@ -211,7 +234,7 @@ class TokenView(View):
|
||||||
# We don't need to store the code anymore.
|
# We don't need to store the code anymore.
|
||||||
self.params.authorization_code.delete()
|
self.params.authorization_code.delete()
|
||||||
|
|
||||||
response_dict = {
|
return {
|
||||||
"access_token": refresh_token.access_token,
|
"access_token": refresh_token.access_token,
|
||||||
"refresh_token": refresh_token.refresh_token,
|
"refresh_token": refresh_token.refresh_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
|
@ -223,9 +246,7 @@ class TokenView(View):
|
||||||
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return response_dict
|
def create_refresh_response(self) -> dict[str, Any]:
|
||||||
|
|
||||||
def create_refresh_response_dic(self) -> dict[str, Any]:
|
|
||||||
"""See https://tools.ietf.org/html/rfc6749#section-6"""
|
"""See https://tools.ietf.org/html/rfc6749#section-6"""
|
||||||
|
|
||||||
unauthorized_scopes = set(self.params.scope) - set(
|
unauthorized_scopes = set(self.params.scope) - set(
|
||||||
|
@ -253,10 +274,11 @@ class TokenView(View):
|
||||||
# Store the refresh_token.
|
# Store the refresh_token.
|
||||||
refresh_token.save()
|
refresh_token.save()
|
||||||
|
|
||||||
# Forget the old token.
|
# Mark old token as revoked
|
||||||
self.params.refresh_token.delete()
|
self.params.refresh_token.revoked = True
|
||||||
|
self.params.refresh_token.save()
|
||||||
|
|
||||||
dic = {
|
return {
|
||||||
"access_token": refresh_token.access_token,
|
"access_token": refresh_token.access_token,
|
||||||
"refresh_token": refresh_token.refresh_token,
|
"refresh_token": refresh_token.refresh_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
|
@ -267,5 +289,3 @@ class TokenView(View):
|
||||||
),
|
),
|
||||||
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
|
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""OAuth Callback Views"""
|
"""OAuth Callback Views"""
|
||||||
|
from json import JSONDecodeError
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -10,6 +11,7 @@ from django.views.generic import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.views.base import OAuthClientMixin
|
from authentik.sources.oauth.views.base import OAuthClientMixin
|
||||||
|
|
||||||
|
@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||||
if "error" in token:
|
if "error" in token:
|
||||||
return self.handle_login_failure(token["error"])
|
return self.handle_login_failure(token["error"])
|
||||||
# Fetch profile info
|
# Fetch profile info
|
||||||
raw_info = client.get_profile_info(token)
|
try:
|
||||||
if raw_info is None:
|
raw_info = client.get_profile_info(token)
|
||||||
|
if raw_info is None:
|
||||||
|
return self.handle_login_failure("Could not retrieve profile.")
|
||||||
|
except JSONDecodeError as exc:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message="Failed to JSON-decode profile.",
|
||||||
|
raw_profile=exc.doc,
|
||||||
|
).from_http(self.request)
|
||||||
return self.handle_login_failure("Could not retrieve profile.")
|
return self.handle_login_failure("Could not retrieve profile.")
|
||||||
identifier = self.get_user_id(raw_info)
|
identifier = self.get_user_id(raw_info)
|
||||||
if identifier is None:
|
if identifier is None:
|
||||||
|
|
|
@ -24,6 +24,10 @@ LOGGER = get_logger()
|
||||||
class UserWriteStageView(StageView):
|
class UserWriteStageView(StageView):
|
||||||
"""Finalise Enrollment flow by creating a user object."""
|
"""Finalise Enrollment flow by creating a user object."""
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Wrapper for post requests"""
|
||||||
|
return self.get(request)
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Save data in the current flow to the currently pending user. If no user is pending,
|
"""Save data in the current flow to the currently pending user. If no user is pending,
|
||||||
a new user is created."""
|
a new user is created."""
|
||||||
|
|
|
@ -21,7 +21,7 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
|
@ -44,7 +44,7 @@ services:
|
||||||
- "0.0.0.0:9000:9000"
|
- "0.0.0.0:9000:9000"
|
||||||
- "0.0.0.0:9443:9443"
|
- "0.0.0.0:9443:9443"
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
|
|
|
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
|
||||||
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
|
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2021.6.3"
|
const VERSION = "2021.6.4"
|
||||||
|
|
|
@ -99,6 +99,11 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
|
||||||
|
|
||||||
|
// Old fields for backwards compatibility
|
||||||
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{BoolToString(*u.IsActive)}})
|
||||||
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
||||||
|
|
||||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}})
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}})
|
||||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,19 @@ import (
|
||||||
|
|
||||||
func (ws *WebServer) configureStatic() {
|
func (ws *WebServer) configureStatic() {
|
||||||
statRouter := ws.lh.NewRoute().Subrouter()
|
statRouter := ws.lh.NewRoute().Subrouter()
|
||||||
|
// Media files, always local
|
||||||
|
fs := http.FileServer(http.Dir(config.G.Paths.Media))
|
||||||
if config.G.Debug || config.G.Web.LoadLocalFiles {
|
if config.G.Debug || config.G.Web.LoadLocalFiles {
|
||||||
ws.log.Debug("Using local static files")
|
ws.log.Debug("Using local static files")
|
||||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
statRouter.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"))))
|
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||||
|
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
|
||||||
} else {
|
} else {
|
||||||
statRouter.Use(ws.staticHeaderMiddleware)
|
statRouter.Use(ws.staticHeaderMiddleware)
|
||||||
ws.log.Debug("Using packaged static files with aggressive caching")
|
ws.log.Debug("Using packaged static files with aggressive caching")
|
||||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
|
statRouter.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))))
|
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
|
||||||
|
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
|
||||||
}
|
}
|
||||||
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header()["Content-Type"] = []string{"text/plain"}
|
rw.Header()["Content-Type"] = []string{"text/plain"}
|
||||||
|
@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() {
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(staticWeb.SecurityTxt)
|
rw.Write(staticWeb.SecurityTxt)
|
||||||
})
|
})
|
||||||
// Media files, always local
|
|
||||||
ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {
|
func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {
|
||||||
|
|
18
schema.yml
18
schema.yml
|
@ -1,7 +1,7 @@
|
||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2021.6.3
|
version: 2021.6.4
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@beryju.org
|
email: hello@beryju.org
|
||||||
|
@ -3096,7 +3096,11 @@ paths:
|
||||||
$ref: '#/components/schemas/Link'
|
$ref: '#/components/schemas/Link'
|
||||||
description: ''
|
description: ''
|
||||||
'404':
|
'404':
|
||||||
description: No recovery flow found.
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Link'
|
||||||
|
description: ''
|
||||||
'400':
|
'400':
|
||||||
$ref: '#/components/schemas/ValidationError'
|
$ref: '#/components/schemas/ValidationError'
|
||||||
'403':
|
'403':
|
||||||
|
@ -18637,7 +18641,10 @@ components:
|
||||||
title: Kp uuid
|
title: Kp uuid
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
fingerprint:
|
fingerprint_sha256:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
fingerprint_sha1:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
cert_expiry:
|
cert_expiry:
|
||||||
|
@ -18660,7 +18667,8 @@ components:
|
||||||
- cert_expiry
|
- cert_expiry
|
||||||
- cert_subject
|
- cert_subject
|
||||||
- certificate_download_url
|
- certificate_download_url
|
||||||
- fingerprint
|
- fingerprint_sha1
|
||||||
|
- fingerprint_sha256
|
||||||
- name
|
- name
|
||||||
- pk
|
- pk
|
||||||
- private_key_available
|
- private_key_available
|
||||||
|
@ -26707,6 +26715,8 @@ components:
|
||||||
id_token:
|
id_token:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
revoked:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- id_token
|
- id_token
|
||||||
- is_expired
|
- is_expired
|
||||||
|
|
|
@ -195,6 +195,8 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
"goauthentik.io/ldap/user",
|
"goauthentik.io/ldap/user",
|
||||||
],
|
],
|
||||||
"memberOf": [],
|
"memberOf": [],
|
||||||
|
"accountStatus": ["true"],
|
||||||
|
"superuser": ["false"],
|
||||||
"goauthentik.io/ldap/active": ["true"],
|
"goauthentik.io/ldap/active": ["true"],
|
||||||
"goauthentik.io/ldap/superuser": ["false"],
|
"goauthentik.io/ldap/superuser": ["false"],
|
||||||
"goauthentik.io/user/override-ips": ["true"],
|
"goauthentik.io/user/override-ips": ["true"],
|
||||||
|
@ -218,6 +220,8 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
"memberOf": [
|
"memberOf": [
|
||||||
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||||
],
|
],
|
||||||
|
"accountStatus": ["true"],
|
||||||
|
"superuser": ["true"],
|
||||||
"goauthentik.io/ldap/active": ["true"],
|
"goauthentik.io/ldap/active": ["true"],
|
||||||
"goauthentik.io/ldap/superuser": ["true"],
|
"goauthentik.io/ldap/superuser": ["true"],
|
||||||
"extraAttribute": ["bar"],
|
"extraAttribute": ["bar"],
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@sentry/browser": "^6.8.0",
|
"@sentry/browser": "^6.8.0",
|
||||||
"@sentry/tracing": "^6.8.0",
|
"@sentry/tracing": "^6.8.0",
|
||||||
"@types/chart.js": "^2.9.32",
|
"@types/chart.js": "^2.9.33",
|
||||||
"@types/codemirror": "5.60.1",
|
"@types/codemirror": "5.60.1",
|
||||||
"@types/grecaptcha": "^3.0.2",
|
"@types/grecaptcha": "^3.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||||
|
@ -35,11 +35,11 @@
|
||||||
"authentik-api": "file:api",
|
"authentik-api": "file:api",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^3.4.0",
|
"chart.js": "^3.4.1",
|
||||||
"chartjs-adapter-moment": "^1.0.0",
|
"chartjs-adapter-moment": "^1.0.0",
|
||||||
"codemirror": "^5.62.0",
|
"codemirror": "^5.62.0",
|
||||||
"construct-style-sheets-polyfill": "^2.4.16",
|
"construct-style-sheets-polyfill": "^2.4.16",
|
||||||
"eslint": "^7.29.0",
|
"eslint": "^7.30.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-custom-elements": "0.0.2",
|
"eslint-plugin-custom-elements": "0.0.2",
|
||||||
"eslint-plugin-lit": "^1.5.1",
|
"eslint-plugin-lit": "^1.5.1",
|
||||||
|
@ -61,12 +61,13 @@
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"webcomponent-qr-code": "^1.0.5",
|
"webcomponent-qr-code": "^1.0.5",
|
||||||
"yaml": "^1.10.2"
|
"yaml": "^1.10.2"
|
||||||
}
|
},
|
||||||
|
"devDependencies": {}
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"name": "authentik-api",
|
"name": "authentik-api",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^3.6"
|
"typescript": "^3.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
"version": "3.9.9",
|
"version": "3.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -1739,6 +1741,24 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@humanwhocodes/object-schema": "^1.2.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@humanwhocodes/object-schema": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
|
||||||
|
},
|
||||||
"node_modules/@jest/types": {
|
"node_modules/@jest/types": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||||
|
@ -2434,9 +2454,9 @@
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/chart.js": {
|
"node_modules/@types/chart.js": {
|
||||||
"version": "2.9.32",
|
"version": "2.9.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"moment": "^2.10.2"
|
"moment": "^2.10.2"
|
||||||
}
|
}
|
||||||
|
@ -3316,9 +3336,9 @@
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||||
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ=="
|
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||||
},
|
},
|
||||||
"node_modules/chartjs-adapter-moment": {
|
"node_modules/chartjs-adapter-moment": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -3861,12 +3881,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "7.29.0",
|
"version": "7.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "7.12.11",
|
"@babel/code-frame": "7.12.11",
|
||||||
"@eslint/eslintrc": "^0.4.2",
|
"@eslint/eslintrc": "^0.4.2",
|
||||||
|
"@humanwhocodes/config-array": "^0.5.0",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.2",
|
"cross-spawn": "^7.0.2",
|
||||||
|
@ -9191,6 +9212,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
||||||
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
|
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
|
||||||
},
|
},
|
||||||
|
"@humanwhocodes/config-array": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
|
||||||
|
"requires": {
|
||||||
|
"@humanwhocodes/object-schema": "^1.2.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@humanwhocodes/object-schema": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
|
||||||
|
},
|
||||||
"@jest/types": {
|
"@jest/types": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||||
|
@ -9780,9 +9816,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/chart.js": {
|
"@types/chart.js": {
|
||||||
"version": "2.9.32",
|
"version": "2.9.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"moment": "^2.10.2"
|
"moment": "^2.10.2"
|
||||||
}
|
}
|
||||||
|
@ -10170,7 +10206,8 @@
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "3.9.9",
|
"version": "3.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w=="
|
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10461,9 +10498,9 @@
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
},
|
},
|
||||||
"chart.js": {
|
"chart.js": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||||
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ=="
|
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||||
},
|
},
|
||||||
"chartjs-adapter-moment": {
|
"chartjs-adapter-moment": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -10899,12 +10936,13 @@
|
||||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||||
},
|
},
|
||||||
"eslint": {
|
"eslint": {
|
||||||
"version": "7.29.0",
|
"version": "7.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/code-frame": "7.12.11",
|
"@babel/code-frame": "7.12.11",
|
||||||
"@eslint/eslintrc": "^0.4.2",
|
"@eslint/eslintrc": "^0.4.2",
|
||||||
|
"@humanwhocodes/config-array": "^0.5.0",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.2",
|
"cross-spawn": "^7.0.2",
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@sentry/browser": "^6.8.0",
|
"@sentry/browser": "^6.8.0",
|
||||||
"@sentry/tracing": "^6.8.0",
|
"@sentry/tracing": "^6.8.0",
|
||||||
"@types/chart.js": "^2.9.32",
|
"@types/chart.js": "^2.9.33",
|
||||||
"@types/codemirror": "5.60.1",
|
"@types/codemirror": "5.60.1",
|
||||||
"@types/grecaptcha": "^3.0.2",
|
"@types/grecaptcha": "^3.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||||
|
@ -64,11 +64,11 @@
|
||||||
"authentik-api": "file:api",
|
"authentik-api": "file:api",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^3.4.0",
|
"chart.js": "^3.4.1",
|
||||||
"chartjs-adapter-moment": "^1.0.0",
|
"chartjs-adapter-moment": "^1.0.0",
|
||||||
"codemirror": "^5.62.0",
|
"codemirror": "^5.62.0",
|
||||||
"construct-style-sheets-polyfill": "^2.4.16",
|
"construct-style-sheets-polyfill": "^2.4.16",
|
||||||
"eslint": "^7.29.0",
|
"eslint": "^7.30.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-custom-elements": "0.0.2",
|
"eslint-plugin-custom-elements": "0.0.2",
|
||||||
"eslint-plugin-lit": "^1.5.1",
|
"eslint-plugin-lit": "^1.5.1",
|
||||||
|
|
|
@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
|
||||||
|
|
||||||
post(context: ResponseContext): Promise<Response | void> {
|
post(context: ResponseContext): Promise<Response | void> {
|
||||||
tenant().then(tenant => {
|
tenant().then(tenant => {
|
||||||
console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`);
|
let msg = `authentik/api[${tenant.matchedDomain}]: `;
|
||||||
|
msg += `${context.response.status} ${context.init.method} ${context.url}`;
|
||||||
|
if (context.response.status >= 400) {
|
||||||
|
context.response.text().then(t => {
|
||||||
|
msg += ` => ${t}`;
|
||||||
|
console.debug(msg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.debug(msg);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return Promise.resolve(context.response);
|
return Promise.resolve(context.response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2021.6.3";
|
export const VERSION = "2021.6.4";
|
||||||
export const PAGE_SIZE = 20;
|
export const PAGE_SIZE = 20;
|
||||||
export const EVENT_REFRESH = "ak-refresh";
|
export const EVENT_REFRESH = "ak-refresh";
|
||||||
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
||||||
|
|
|
@ -12,6 +12,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
||||||
if (this.isInViewport) {
|
if (this.isInViewport) {
|
||||||
this.loadInstance(value).then(instance => {
|
this.loadInstance(value).then(instance => {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
|
this.requestUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +38,11 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetForm(): void {
|
||||||
|
this.instance = undefined;
|
||||||
|
this._initialLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
// if we're in viewport now and haven't loaded AND have a PK set, load now
|
// if we're in viewport now and haven't loaded AND have a PK set, load now
|
||||||
if (this.isInViewport && !this._initialLoad && this._instancePk) {
|
if (this.isInViewport && !this._initialLoad && this._instancePk) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
||||||
columns(): TableColumn[] {
|
columns(): TableColumn[] {
|
||||||
return [
|
return [
|
||||||
new TableColumn(t`Provider`, "provider"),
|
new TableColumn(t`Provider`, "provider"),
|
||||||
|
new TableColumn(t`Revoked?`, "revoked"),
|
||||||
new TableColumn(t`Expires`, "expires"),
|
new TableColumn(t`Expires`, "expires"),
|
||||||
new TableColumn(t`Scopes`, "scope"),
|
new TableColumn(t`Scopes`, "scope"),
|
||||||
new TableColumn(""),
|
new TableColumn(""),
|
||||||
|
@ -62,6 +63,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
||||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||||
${item.provider?.name}
|
${item.provider?.name}
|
||||||
</a>`,
|
</a>`,
|
||||||
|
html`${item.revoked ? t`Yes` : t`No`}`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires?.toLocaleString()}`,
|
||||||
html`${item.scope.join(", ")}`,
|
html`${item.scope.join(", ")}`,
|
||||||
html`
|
html`
|
||||||
|
|
|
@ -225,7 +225,14 @@ export abstract class Table<T> extends LitElement {
|
||||||
|
|
||||||
renderToolbar(): TemplateResult {
|
renderToolbar(): TemplateResult {
|
||||||
return html`<button
|
return html`<button
|
||||||
@click=${() => { this.fetch(); }}
|
@click=${() => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
class="pf-c-button pf-m-primary">
|
class="pf-c-button pf-m-primary">
|
||||||
${t`Refresh`}
|
${t`Refresh`}
|
||||||
</button>`;
|
</button>`;
|
||||||
|
@ -241,7 +248,12 @@ export abstract class Table<T> extends LitElement {
|
||||||
}
|
}
|
||||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||||
this.search = value;
|
this.search = value;
|
||||||
this.fetch();
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
}}>
|
}}>
|
||||||
</ak-table-search> `;
|
</ak-table-search> `;
|
||||||
}
|
}
|
||||||
|
@ -274,7 +286,15 @@ export abstract class Table<T> extends LitElement {
|
||||||
<ak-table-pagination
|
<ak-table-pagination
|
||||||
class="pf-c-toolbar__item pf-m-pagination"
|
class="pf-c-toolbar__item pf-m-pagination"
|
||||||
.pages=${this.data?.pagination}
|
.pages=${this.data?.pagination}
|
||||||
.pageChangeHandler=${(page: number) => { this.page = page; this.fetch(); }}>
|
.pageChangeHandler=${(page: number) => {
|
||||||
|
this.page = page;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}>
|
||||||
</ak-table-pagination>
|
</ak-table-pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -300,7 +320,15 @@ export abstract class Table<T> extends LitElement {
|
||||||
<ak-table-pagination
|
<ak-table-pagination
|
||||||
class="pf-c-toolbar__item pf-m-pagination"
|
class="pf-c-toolbar__item pf-m-pagination"
|
||||||
.pages=${this.data?.pagination}
|
.pages=${this.data?.pagination}
|
||||||
.pageChangeHandler=${(page: number) => { this.page = page; this.fetch(); }}>
|
.pageChangeHandler=${(page: number) => {
|
||||||
|
this.page = page;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}>
|
||||||
</ak-table-pagination>
|
</ak-table-pagination>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -488,8 +488,12 @@ msgid "Certificate"
|
||||||
msgstr "Certificate"
|
msgstr "Certificate"
|
||||||
|
|
||||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||||
msgid "Certificate Fingerprint"
|
msgid "Certificate Fingerprint (SHA1)"
|
||||||
msgstr "Certificate Fingerprint"
|
msgstr "Certificate Fingerprint (SHA1)"
|
||||||
|
|
||||||
|
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||||
|
msgid "Certificate Fingerprint (SHA256)"
|
||||||
|
msgstr "Certificate Fingerprint (SHA256)"
|
||||||
|
|
||||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||||
msgid "Certificate Subjet"
|
msgid "Certificate Subjet"
|
||||||
|
@ -2346,6 +2350,7 @@ msgstr "Negates the outcome of the binding. Messages are unaffected."
|
||||||
msgid "New version available!"
|
msgid "New version available!"
|
||||||
msgstr "New version available!"
|
msgstr "New version available!"
|
||||||
|
|
||||||
|
#: src/elements/oauth/UserRefreshList.ts
|
||||||
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
||||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||||
#: src/pages/groups/GroupListPage.ts
|
#: src/pages/groups/GroupListPage.ts
|
||||||
|
@ -3044,6 +3049,10 @@ msgstr "Return home"
|
||||||
msgid "Return to device picker"
|
msgid "Return to device picker"
|
||||||
msgstr "Return to device picker"
|
msgstr "Return to device picker"
|
||||||
|
|
||||||
|
#: src/elements/oauth/UserRefreshList.ts
|
||||||
|
msgid "Revoked?"
|
||||||
|
msgstr "Revoked?"
|
||||||
|
|
||||||
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
||||||
msgid "SAML Attribute Name"
|
msgid "SAML Attribute Name"
|
||||||
msgstr "SAML Attribute Name"
|
msgstr "SAML Attribute Name"
|
||||||
|
@ -4544,6 +4553,7 @@ msgstr ""
|
||||||
msgid "X509 Subject"
|
msgid "X509 Subject"
|
||||||
msgstr "X509 Subject"
|
msgstr "X509 Subject"
|
||||||
|
|
||||||
|
#: src/elements/oauth/UserRefreshList.ts
|
||||||
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
||||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||||
#: src/pages/groups/GroupListPage.ts
|
#: src/pages/groups/GroupListPage.ts
|
||||||
|
|
|
@ -484,7 +484,11 @@ msgid "Certificate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Certificate Fingerprint"
|
msgid "Certificate Fingerprint (SHA1)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Certificate Fingerprint (SHA256)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
|
@ -2350,6 +2354,7 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
#:
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -3036,6 +3041,10 @@ msgstr ""
|
||||||
msgid "Return to device picker"
|
msgid "Return to device picker"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Revoked?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "SAML Attribute Name"
|
msgid "SAML Attribute Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -4539,6 +4548,7 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
#:
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -103,10 +103,18 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
||||||
<dl class="pf-c-description-list pf-m-horizontal">
|
<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
<dt class="pf-c-description-list__term">
|
<dt class="pf-c-description-list__term">
|
||||||
<span class="pf-c-description-list__text">${t`Certificate Fingerprint`}</span>
|
<span class="pf-c-description-list__text">${t`Certificate Fingerprint (SHA1)`}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">${item.fingerprint}</div>
|
<div class="pf-c-description-list__text">${item.fingerprintSha1}</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-description-list__group">
|
||||||
|
<dt class="pf-c-description-list__term">
|
||||||
|
<span class="pf-c-description-list__text">${t`Certificate Fingerprint (SHA256)`}</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="pf-c-description-list__description">
|
||||||
|
<div class="pf-c-description-list__text">${item.fingerprintSha256}</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class OutpostHealthElement extends LitElement {
|
||||||
outpostId?: string;
|
outpostId?: string;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({attribute: false})
|
||||||
outpostHealth: OutpostHealth[] = [];
|
outpostHealth?: OutpostHealth[];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, AKGlobal];
|
return [PFBase, AKGlobal];
|
||||||
|
@ -23,7 +23,8 @@ export class OutpostHealthElement extends LitElement {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addEventListener(EVENT_REFRESH, () => {
|
window.addEventListener(EVENT_REFRESH, () => {
|
||||||
|
this.outpostHealth = undefined;
|
||||||
this.firstUpdated();
|
this.firstUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -38,7 +39,7 @@ export class OutpostHealthElement extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.outpostId) {
|
if (!this.outpostId || !this.outpostHealth) {
|
||||||
return html`<ak-spinner></ak-spinner>`;
|
return html`<ak-spinner></ak-spinner>`;
|
||||||
}
|
}
|
||||||
if (this.outpostHealth.length === 0) {
|
if (this.outpostHealth.length === 0) {
|
||||||
|
|
|
@ -9,13 +9,14 @@ import "../../elements/buttons/ActionButton";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { PAGE_SIZE } from "../../constants";
|
||||||
import { CoreApi, User } from "authentik-api";
|
import { CoreApi, User } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
|
||||||
import "../../elements/forms/DeleteForm";
|
import "../../elements/forms/DeleteForm";
|
||||||
import "./UserActiveForm";
|
import "./UserActiveForm";
|
||||||
import "./UserForm";
|
import "./UserForm";
|
||||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||||
import { MessageLevel } from "../../elements/messages/Message";
|
import { MessageLevel } from "../../elements/messages/Message";
|
||||||
import { first } from "../../utils";
|
import { first } from "../../utils";
|
||||||
|
import { until } from "lit-html/directives/until";
|
||||||
|
|
||||||
@customElement("ak-user-list")
|
@customElement("ak-user-list")
|
||||||
export class UserListPage extends TablePage<User> {
|
export class UserListPage extends TablePage<User> {
|
||||||
|
@ -128,27 +129,33 @@ export class UserListPage extends TablePage<User> {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ak-dropdown>
|
</ak-dropdown>
|
||||||
<ak-action-button
|
${until(tenant().then(te => {
|
||||||
.apiRequest=${() => {
|
if (te.flowRecovery) {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({
|
return html`
|
||||||
id: item.pk || 0,
|
<ak-action-button
|
||||||
}).then(rec => {
|
.apiRequest=${() => {
|
||||||
showMessage({
|
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({
|
||||||
level: MessageLevel.success,
|
id: item.pk || 0,
|
||||||
message: t`Successfully generated recovery link`,
|
}).then(rec => {
|
||||||
description: rec.link
|
showMessage({
|
||||||
});
|
level: MessageLevel.success,
|
||||||
}).catch((ex: Response) => {
|
message: t`Successfully generated recovery link`,
|
||||||
ex.json().then(() => {
|
description: rec.link
|
||||||
showMessage({
|
});
|
||||||
level: MessageLevel.error,
|
}).catch((ex: Response) => {
|
||||||
message: t`No recovery flow is configured.`,
|
ex.json().then(() => {
|
||||||
});
|
showMessage({
|
||||||
});
|
level: MessageLevel.error,
|
||||||
});
|
message: t`No recovery flow is configured.`,
|
||||||
}}>
|
});
|
||||||
${t`Reset Password`}
|
});
|
||||||
</ak-action-button>
|
});
|
||||||
|
}}>
|
||||||
|
${t`Reset Password`}
|
||||||
|
</ak-action-button>`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
}))}
|
||||||
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
|
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
|
||||||
${t`Impersonate`}
|
${t`Impersonate`}
|
||||||
</a>`,
|
</a>`,
|
||||||
|
|
|
@ -6,17 +6,17 @@ This installation method is for test-setups and small-scale productive setups.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- A Linux host with at least 2 CPU cores and 4 GB of RAM.
|
- A Linux host with at least 2 CPU cores and 2 GB of RAM.
|
||||||
- docker
|
- docker
|
||||||
- docker-compose
|
- docker-compose
|
||||||
|
|
||||||
## Preparation
|
## Preparation
|
||||||
|
|
||||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.6.3/docker-compose.yml). Place it in a directory of your choice.
|
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.6.4/docker-compose.yml). Place it in a directory of your choice.
|
||||||
|
|
||||||
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
|
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||||
|
|
||||||
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.6.3 >> .env`
|
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.6.4 >> .env`
|
||||||
|
|
||||||
If this is a fresh authentik install run the following commands to generate a password:
|
If this is a fresh authentik install run the following commands to generate a password:
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,14 @@ Create an application in authentik and note the slug, as this will be used later
|
||||||
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
||||||
- Audience: `https://gitlab.company`
|
- Audience: `https://gitlab.company`
|
||||||
- Issuer: `https://gitlab.company`
|
- Issuer: `https://gitlab.company`
|
||||||
- Binding: `Post`
|
- Binding: `Redirect`
|
||||||
|
|
||||||
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
Under *Advanced protocol settings*, set a certificate for *Signing Certificate*.
|
||||||
|
|
||||||
## GitLab Configuration
|
## GitLab Configuration
|
||||||
|
|
||||||
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
|
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
|
||||||
|
To get the value for `idp_cert_fingerprint`, go to the Certificate list under *Identity & Cryptography*, and expand the selected certificate.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
gitlab_rails['omniauth_enabled'] = true
|
gitlab_rails['omniauth_enabled'] = true
|
||||||
|
@ -46,7 +47,7 @@ gitlab_rails['omniauth_providers'] = [
|
||||||
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
||||||
# Shown when navigating to certificates in authentik
|
# Shown when navigating to certificates in authentik
|
||||||
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
|
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
|
||||||
idp_sso_target_url: 'https://authentik.company/application/saml/<authentik application slug>/sso/binding/post/',
|
idp_sso_target_url: 'https://authentik.company/application/saml/<authentik application slug>/sso/binding/redirect/',
|
||||||
issuer: 'https://gitlab.company',
|
issuer: 'https://gitlab.company',
|
||||||
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||||
attribute_statements: {
|
attribute_statements: {
|
||||||
|
|
|
@ -11,7 +11,7 @@ version: "3.5"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
authentik_proxy:
|
authentik_proxy:
|
||||||
image: ghcr.io/goauthentik/proxy:2021.6.3
|
image: ghcr.io/goauthentik/proxy:2021.6.4
|
||||||
ports:
|
ports:
|
||||||
- 4180:4180
|
- 4180:4180
|
||||||
- 4443:4443
|
- 4443:4443
|
||||||
|
@ -21,7 +21,7 @@ services:
|
||||||
AUTHENTIK_TOKEN: token-generated-by-authentik
|
AUTHENTIK_TOKEN: token-generated-by-authentik
|
||||||
# Or, for the LDAP Outpost
|
# Or, for the LDAP Outpost
|
||||||
authentik_proxy:
|
authentik_proxy:
|
||||||
image: ghcr.io/goauthentik/ldap:2021.6.3
|
image: ghcr.io/goauthentik/ldap:2021.6.4
|
||||||
ports:
|
ports:
|
||||||
- 389:3389
|
- 389:3389
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -14,7 +14,7 @@ metadata:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
name: authentik-outpost-api
|
name: authentik-outpost-api
|
||||||
stringData:
|
stringData:
|
||||||
authentik_host: "__AUTHENTIK_URL__"
|
authentik_host: "__AUTHENTIK_URL__"
|
||||||
|
@ -29,7 +29,7 @@ metadata:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
name: authentik-outpost
|
name: authentik-outpost
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
|
@ -54,7 +54,7 @@ metadata:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
name: authentik-outpost
|
name: authentik-outpost
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
|
@ -62,14 +62,14 @@ spec:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- env:
|
- env:
|
||||||
|
@ -88,7 +88,7 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
key: authentik_host_insecure
|
key: authentik_host_insecure
|
||||||
name: authentik-outpost-api
|
name: authentik-outpost-api
|
||||||
image: ghcr.io/goauthentik/proxy:2021.6.3
|
image: ghcr.io/goauthentik/proxy:2021.6.4
|
||||||
name: proxy
|
name: proxy
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 4180
|
- containerPort: 4180
|
||||||
|
@ -110,7 +110,7 @@ metadata:
|
||||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||||
app.kubernetes.io/managed-by: goauthentik.io
|
app.kubernetes.io/managed-by: goauthentik.io
|
||||||
app.kubernetes.io/name: authentik-proxy
|
app.kubernetes.io/name: authentik-proxy
|
||||||
app.kubernetes.io/version: 2021.6.3
|
app.kubernetes.io/version: 2021.6.4
|
||||||
name: authentik-outpost
|
name: authentik-outpost
|
||||||
spec:
|
spec:
|
||||||
rules:
|
rules:
|
||||||
|
|
|
@ -139,6 +139,28 @@ slug: "2021.6"
|
||||||
- web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form
|
- web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form
|
||||||
- web/admin: fix text color on pf-c-card
|
- web/admin: fix text color on pf-c-card
|
||||||
|
|
||||||
|
## Fixed in 2021.6.4
|
||||||
|
|
||||||
|
- core: only show `Reset password` link when recovery flow is configured
|
||||||
|
- crypto: show both sha1 and sha256 fingerprints
|
||||||
|
- flows: handle old cached flow plans better
|
||||||
|
- g: fix static and media caching not working properly
|
||||||
|
- outposts: fix container not being started after creation
|
||||||
|
- outposts: fix docker controller not checking env correctly
|
||||||
|
- outposts: fix docker controller not checking ports correctly
|
||||||
|
- outposts: fix empty message when docker outpost controller has changed nothing
|
||||||
|
- outposts: fix permissions not being set correctly upon outpost creation
|
||||||
|
- outposts/ldap: add support for boolean fields in ldap
|
||||||
|
- outposts/proxy: always redirect to session-end interface on sign_out
|
||||||
|
- providers/oauth2: add revoked field, create suspicious event when previous token is used
|
||||||
|
- providers/oauth2: deepmerge claims
|
||||||
|
- providers/oauth2: fix CORS headers not being set for unsuccessful requests
|
||||||
|
- providers/oauth2: use self.expires for exp field instead of calculating it again
|
||||||
|
- sources/oauth: create configuration error event when profile can't be parsed as json
|
||||||
|
- stages/user_write: add wrapper for post to user_write
|
||||||
|
- web/admin: fix ModelForm not re-loading after being reset
|
||||||
|
- web/admin: show oauth2 token revoked status
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
This release does not introduce any new requirements.
|
This release does not introduce any new requirements.
|
||||||
|
|
Reference in New Issue