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]
|
||||
current_version = 2021.6.3
|
||||
current_version = 2021.6.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
|
|
@ -33,14 +33,14 @@ jobs:
|
|||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.6.3,
|
||||
beryju/authentik:2021.6.4,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.6.3,
|
||||
ghcr.io/goauthentik/server:2021.6.4,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- 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: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
|
@ -75,14 +75,14 @@ jobs:
|
|||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.6.3,
|
||||
beryju/authentik-proxy:2021.6.4,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.6.3,
|
||||
ghcr.io/goauthentik/proxy:2021.6.4,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- 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: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
|
@ -117,14 +117,14 @@ jobs:
|
|||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.6.3,
|
||||
beryju/authentik-ldap:2021.6.4,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.6.3,
|
||||
ghcr.io/goauthentik/ldap:2021.6.4,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- 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: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
|
@ -176,7 +176,6 @@ jobs:
|
|||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.6.3
|
||||
version: authentik@2021.6.4
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
finalize: false
|
||||
|
|
|
@ -122,19 +122,19 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:055f9dc07f95f202a4dc25196a3a9f1e2f137171ee364cf980e4673de75fb529",
|
||||
"sha256:bc9b278e362ec9b531511a498262297f074c4f5ca9560455919a0af1a4698615"
|
||||
"sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03",
|
||||
"sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.104"
|
||||
"version": "==1.17.105"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:23aa3238c004319f78423eb8cbba2813b62ee64d0e3bab04e0a00e067f99542a",
|
||||
"sha256:95ab472c8254b8d2cfa6d719b433e511fbcf80895b4cd18e4219c1efa0b78270"
|
||||
"sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c",
|
||||
"sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67"
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
|
@ -778,11 +778,11 @@
|
|||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
|
@ -1585,7 +1585,7 @@
|
|||
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
|
||||
"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"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
|
@ -1632,11 +1632,11 @@
|
|||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
"""authentik"""
|
||||
__version__ = "2021.6.3"
|
||||
__version__ = "2021.6.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
from json import loads
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http.response import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django_filters.filters import BooleanFilter, CharFilter
|
||||
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 rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
|
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||
@extend_schema(
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
"404": OpenApiResponse(description="No recovery flow found."),
|
||||
"404": LinkSerializer(many=False),
|
||||
},
|
||||
)
|
||||
@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
|
||||
flow = tenant.flow_recovery
|
||||
if not flow:
|
||||
raise Http404
|
||||
return Response({"link": ""}, status=404)
|
||||
user: User = self.get_object()
|
||||
token, __ = Token.objects.get_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
|
|
|
@ -14,7 +14,9 @@ def is_dict(value: Any):
|
|||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
if isinstance(value, dict):
|
||||
return
|
||||
raise ValidationError("Value must be a dictionary.")
|
||||
raise ValidationError(
|
||||
"Value must be a dictionary, and not have any duplicate keys."
|
||||
)
|
||||
|
||||
|
||||
class PassiveSerializer(Serializer):
|
||||
|
|
|
@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"fingerprint",
|
||||
"fingerprint_sha256",
|
||||
"fingerprint_sha1",
|
||||
"certificate_data",
|
||||
"key_data",
|
||||
"cert_expiry",
|
||||
|
|
|
@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
|||
return self._private_key
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
def fingerprint_sha256(self) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate_data"""
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
||||
"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
|
||||
def kid(self):
|
||||
"""Get Key ID used for JWKS"""
|
||||
|
|
|
@ -3,6 +3,7 @@ from functools import partial
|
|||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
@ -63,7 +64,15 @@ class AuditMiddleware:
|
|||
|
||||
if settings.DEBUG:
|
||||
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(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
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)
|
||||
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:
|
||||
# Early check if theres an active Plan for the current 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", ""))
|
||||
# We don't save the Plan after getting the next stage
|
||||
# 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:
|
||||
self._logger.debug("f(exec): no more stages, flow is done.")
|
||||
return self._flow_done()
|
||||
|
|
|
@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
|
|||
raise ValidationError(
|
||||
(
|
||||
f"Outpost type {self.initial_data['type']} can't be used with "
|
||||
f"{type(provider)} providers."
|
||||
f"{provider.__class__.__name__} providers."
|
||||
)
|
||||
)
|
||||
return providers
|
||||
|
|
|
@ -36,8 +36,10 @@ class DockerController(BaseController):
|
|||
|
||||
def _get_env(self) -> dict[str, str]:
|
||||
return {
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
||||
"AUTHENTIK_INSECURE": str(
|
||||
self.outpost.config.authentik_host_insecure
|
||||
).lower(),
|
||||
"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
|
||||
to be rebuilt."""
|
||||
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():
|
||||
if key not in container_env:
|
||||
continue
|
||||
if container_env[key] != expected_value:
|
||||
entry = f"{key.upper()}={expected_value}"
|
||||
if entry not in container_env:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -62,14 +63,17 @@ class DockerController(BaseController):
|
|||
# When the container isn't running, the API doesn't report any port mappings
|
||||
if container.status != "running":
|
||||
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:
|
||||
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
||||
if key not in container.ports:
|
||||
return True
|
||||
host_matching = False
|
||||
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:
|
||||
return True
|
||||
return False
|
||||
|
@ -79,7 +83,7 @@ class DockerController(BaseController):
|
|||
try:
|
||||
return self.client.containers.get(container_name), False
|
||||
except NotFound:
|
||||
self.logger.info("Container does not exist, creating")
|
||||
self.logger.info("(Re-)creating container...")
|
||||
image_name = self.get_container_image()
|
||||
self.client.images.pull(image_name)
|
||||
container_args = {
|
||||
|
@ -107,6 +111,7 @@ class DockerController(BaseController):
|
|||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
if has_been_created:
|
||||
container.start()
|
||||
return None
|
||||
# Check if the container is out of date, delete it and retry
|
||||
if len(container.image.tags) > 0:
|
||||
|
@ -164,6 +169,7 @@ class DockerController(BaseController):
|
|||
self.logger.info("Container is not running, restarting...")
|
||||
container.start()
|
||||
return None
|
||||
self.logger.info("Container is running")
|
||||
return None
|
||||
except DockerException as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
|
|
@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||
},
|
||||
"outposts_service_connection_check": {
|
||||
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
|
||||
"schedule": crontab(minute="*/60"),
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"outpost_token_ensurer": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""authentik outpost signals"""
|
||||
from django.core.cache import cache
|
||||
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 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)
|
||||
|
||||
|
||||
@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)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance: Model, **_):
|
||||
|
|
|
@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
|||
"expires",
|
||||
"scope",
|
||||
"id_token",
|
||||
"revoked",
|
||||
]
|
||||
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)
|
||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
revoked = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
|
@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
|||
# Convert datetimes into timestamps.
|
||||
now = int(time.time())
|
||||
iat_time = now
|
||||
exp_time = int(
|
||||
now + timedelta_from_string(self.provider.token_validity).total_seconds()
|
||||
)
|
||||
exp_time = int(dateformat.format(self.expires, "U"))
|
||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||
auth_events = Event.objects.filter(
|
||||
action=EventAction.LOGIN, user=get_user(user)
|
||||
|
|
|
@ -6,6 +6,8 @@ from django.urls import reverse
|
|||
from django.utils.encoding import force_str
|
||||
|
||||
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.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
|
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
|
|||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
|
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
|
|||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://testserver",
|
||||
},
|
||||
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)
|
||||
|
||||
def test_request_refresh_token(self):
|
||||
|
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
|
|||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
|
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
|
|||
},
|
||||
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)
|
||||
|
||||
def test_auth_code_view(self):
|
||||
|
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
|
|||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
|
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
|
|||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
|
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
|
|||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
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 structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.errors import BearerTokenError
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
|
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
|
|||
if not allowed:
|
||||
LOGGER.warning(
|
||||
"CORS: Origin is not an allowed origin",
|
||||
requested=origin,
|
||||
requested=received_origin,
|
||||
allowed=allowed_origins,
|
||||
)
|
||||
return response
|
||||
|
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
|
|||
raise BearerTokenError("invalid_token")
|
||||
|
||||
try:
|
||||
kwargs["token"] = RefreshToken.objects.get(
|
||||
token: RefreshToken = RefreshToken.objects.get(
|
||||
access_token=access_token
|
||||
)
|
||||
except RefreshToken.DoesNotExist:
|
||||
LOGGER.debug("Token does not exist", access_token=access_token)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if kwargs["token"].is_expired:
|
||||
if token.is_expired:
|
||||
LOGGER.debug("Token has expired", access_token=access_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(
|
||||
"Scope missmatch.",
|
||||
required=set(scopes),
|
||||
token_has=set(kwargs["token"].scope),
|
||||
token_has=set(token.scope),
|
||||
)
|
||||
raise BearerTokenError("insufficient_scope")
|
||||
except BearerTokenError as error:
|
||||
|
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
|
|||
"WWW-Authenticate"
|
||||
] = f'error="{error.code}", error_description="{error.description}"'
|
||||
return response
|
||||
|
||||
kwargs["token"] = token
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
return view_wrapper
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
|
|||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
|
@ -30,6 +31,7 @@ LOGGER = get_logger()
|
|||
|
||||
|
||||
@dataclass
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class TokenParams:
|
||||
"""Token params"""
|
||||
|
||||
|
@ -40,6 +42,8 @@ class TokenParams:
|
|||
state: str
|
||||
scope: list[str]
|
||||
|
||||
provider: OAuth2Provider
|
||||
|
||||
authorization_code: Optional[AuthorizationCode] = None
|
||||
refresh_token: Optional[RefreshToken] = None
|
||||
|
||||
|
@ -47,35 +51,34 @@ class TokenParams:
|
|||
|
||||
raw_code: InitVar[str] = ""
|
||||
raw_token: InitVar[str] = ""
|
||||
request: InitVar[Optional[HttpRequest]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_request(request: HttpRequest) -> "TokenParams":
|
||||
"""Extract Token Parameters from http request"""
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
|
||||
def parse(
|
||||
request: HttpRequest,
|
||||
provider: OAuth2Provider,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
) -> "TokenParams":
|
||||
"""Parse params for request"""
|
||||
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_secret=client_secret,
|
||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||
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", ""),
|
||||
scope=request.POST.get("scope", "").split(),
|
||||
# PKCE parameter.
|
||||
code_verifier=request.POST.get("code_verifier"),
|
||||
)
|
||||
|
||||
def __post_init__(self, raw_code, raw_token):
|
||||
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")
|
||||
|
||||
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
|
||||
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
||||
if self.provider.client_secret != self.client_secret:
|
||||
LOGGER.warning(
|
||||
|
@ -87,7 +90,6 @@ class TokenParams:
|
|||
|
||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
self.__post_init_code(raw_code)
|
||||
|
||||
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing refresh token")
|
||||
|
@ -107,7 +109,14 @@ class TokenParams:
|
|||
token=raw_token,
|
||||
)
|
||||
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:
|
||||
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
||||
raise TokenError("unsupported_grant_type")
|
||||
|
@ -159,13 +168,14 @@ class TokenParams:
|
|||
class TokenView(View):
|
||||
"""Generate tokens for clients"""
|
||||
|
||||
provider: Optional[OAuth2Provider] = None
|
||||
params: Optional[TokenParams] = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
if self.params:
|
||||
allowed_origins = self.params.provider.redirect_uris.split("\n")
|
||||
if self.provider:
|
||||
allowed_origins = self.provider.redirect_uris.split("\n")
|
||||
cors_allow(self.request, response, *allowed_origins)
|
||||
return response
|
||||
|
||||
|
@ -175,19 +185,32 @@ class TokenView(View):
|
|||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Generate tokens for clients"""
|
||||
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:
|
||||
return TokenResponse(self.create_code_response_dic())
|
||||
return TokenResponse(self.create_code_response())
|
||||
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}")
|
||||
except TokenError as error:
|
||||
return TokenResponse(error.create_dict(), status=400)
|
||||
except UserAuthError as error:
|
||||
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"""
|
||||
|
||||
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.
|
||||
self.params.authorization_code.delete()
|
||||
|
||||
response_dict = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
|
@ -223,9 +246,7 @@ class TokenView(View):
|
|||
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return response_dict
|
||||
|
||||
def create_refresh_response_dic(self) -> dict[str, Any]:
|
||||
def create_refresh_response(self) -> dict[str, Any]:
|
||||
"""See https://tools.ietf.org/html/rfc6749#section-6"""
|
||||
|
||||
unauthorized_scopes = set(self.params.scope) - set(
|
||||
|
@ -253,10 +274,11 @@ class TokenView(View):
|
|||
# Store the refresh_token.
|
||||
refresh_token.save()
|
||||
|
||||
# Forget the old token.
|
||||
self.params.refresh_token.delete()
|
||||
# Mark old token as revoked
|
||||
self.params.refresh_token.revoked = True
|
||||
self.params.refresh_token.save()
|
||||
|
||||
dic = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
|
@ -267,5 +289,3 @@ class TokenView(View):
|
|||
),
|
||||
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""OAuth Callback Views"""
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -10,6 +11,7 @@ from django.views.generic import View
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
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.views.base import OAuthClientMixin
|
||||
|
||||
|
@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
if "error" in token:
|
||||
return self.handle_login_failure(token["error"])
|
||||
# Fetch profile info
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
try:
|
||||
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.")
|
||||
identifier = self.get_user_id(raw_info)
|
||||
if identifier is None:
|
||||
|
|
|
@ -24,6 +24,10 @@ LOGGER = get_logger()
|
|||
class UserWriteStageView(StageView):
|
||||
"""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:
|
||||
"""Save data in the current flow to the currently pending user. If no user is pending,
|
||||
a new user is created."""
|
||||
|
|
|
@ -21,7 +21,7 @@ services:
|
|||
networks:
|
||||
- internal
|
||||
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
|
||||
command: server
|
||||
environment:
|
||||
|
@ -44,7 +44,7 @@ services:
|
|||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
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
|
||||
command: worker
|
||||
networks:
|
||||
|
|
|
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
|
|||
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)})
|
||||
|
||||
// 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/superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
||||
|
||||
|
|
|
@ -10,15 +10,19 @@ import (
|
|||
|
||||
func (ws *WebServer) configureStatic() {
|
||||
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 {
|
||||
ws.log.Debug("Using local static files")
|
||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
||||
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
|
||||
} else {
|
||||
statRouter.Use(ws.staticHeaderMiddleware)
|
||||
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))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
|
||||
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
|
||||
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) {
|
||||
rw.Header()["Content-Type"] = []string{"text/plain"}
|
||||
|
@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() {
|
|||
rw.WriteHeader(200)
|
||||
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 {
|
||||
|
|
18
schema.yml
18
schema.yml
|
@ -1,7 +1,7 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.6.3
|
||||
version: 2021.6.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
|
@ -3096,7 +3096,11 @@ paths:
|
|||
$ref: '#/components/schemas/Link'
|
||||
description: ''
|
||||
'404':
|
||||
description: No recovery flow found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Link'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
|
@ -18637,7 +18641,10 @@ components:
|
|||
title: Kp uuid
|
||||
name:
|
||||
type: string
|
||||
fingerprint:
|
||||
fingerprint_sha256:
|
||||
type: string
|
||||
readOnly: true
|
||||
fingerprint_sha1:
|
||||
type: string
|
||||
readOnly: true
|
||||
cert_expiry:
|
||||
|
@ -18660,7 +18667,8 @@ components:
|
|||
- cert_expiry
|
||||
- cert_subject
|
||||
- certificate_download_url
|
||||
- fingerprint
|
||||
- fingerprint_sha1
|
||||
- fingerprint_sha256
|
||||
- name
|
||||
- pk
|
||||
- private_key_available
|
||||
|
@ -26707,6 +26715,8 @@ components:
|
|||
id_token:
|
||||
type: string
|
||||
readOnly: true
|
||||
revoked:
|
||||
type: boolean
|
||||
required:
|
||||
- id_token
|
||||
- is_expired
|
||||
|
|
|
@ -195,6 +195,8 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||
"goauthentik.io/ldap/user",
|
||||
],
|
||||
"memberOf": [],
|
||||
"accountStatus": ["true"],
|
||||
"superuser": ["false"],
|
||||
"goauthentik.io/ldap/active": ["true"],
|
||||
"goauthentik.io/ldap/superuser": ["false"],
|
||||
"goauthentik.io/user/override-ips": ["true"],
|
||||
|
@ -218,6 +220,8 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||
"memberOf": [
|
||||
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
],
|
||||
"accountStatus": ["true"],
|
||||
"superuser": ["true"],
|
||||
"goauthentik.io/ldap/active": ["true"],
|
||||
"goauthentik.io/ldap/superuser": ["true"],
|
||||
"extraAttribute": ["bar"],
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sentry/browser": "^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/grecaptcha": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||
|
@ -35,11 +35,11 @@
|
|||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.4.0",
|
||||
"chart.js": "^3.4.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.2",
|
||||
"eslint-plugin-lit": "^1.5.1",
|
||||
|
@ -61,12 +61,13 @@
|
|||
"typescript": "^4.3.5",
|
||||
"webcomponent-qr-code": "^1.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"api": {
|
||||
"name": "authentik-api",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^3.6"
|
||||
}
|
||||
},
|
||||
|
@ -74,6 +75,7 @@
|
|||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -1739,6 +1741,24 @@
|
|||
"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": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||
|
@ -2434,9 +2454,9 @@
|
|||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"version": "2.9.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
|
@ -3316,9 +3336,9 @@
|
|||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz",
|
||||
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
},
|
||||
"node_modules/chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
|
@ -3861,12 +3881,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
||||
"version": "7.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.2",
|
||||
"@humanwhocodes/config-array": "^0.5.0",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
|
@ -9191,6 +9212,21 @@
|
|||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
||||
"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": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||
|
@ -9780,9 +9816,9 @@
|
|||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"version": "2.9.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
|
@ -10170,7 +10206,8 @@
|
|||
"typescript": {
|
||||
"version": "3.9.9",
|
||||
"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=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz",
|
||||
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
},
|
||||
"chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
|
@ -10899,12 +10936,13 @@
|
|||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
||||
"version": "7.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.2",
|
||||
"@humanwhocodes/config-array": "^0.5.0",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sentry/browser": "^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/grecaptcha": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||
|
@ -64,11 +64,11 @@
|
|||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.4.0",
|
||||
"chart.js": "^3.4.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.2",
|
||||
"eslint-plugin-lit": "^1.5.1",
|
||||
|
|
|
@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
|
|||
|
||||
post(context: ResponseContext): Promise<Response | void> {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
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 EVENT_REFRESH = "ak-refresh";
|
||||
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) {
|
||||
this.loadInstance(value).then(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 {
|
||||
// if we're in viewport now and haven't loaded AND have a PK set, load now
|
||||
if (this.isInViewport && !this._initialLoad && this._instancePk) {
|
||||
|
|
|
@ -34,6 +34,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
|||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Provider`, "provider"),
|
||||
new TableColumn(t`Revoked?`, "revoked"),
|
||||
new TableColumn(t`Expires`, "expires"),
|
||||
new TableColumn(t`Scopes`, "scope"),
|
||||
new TableColumn(""),
|
||||
|
@ -62,6 +63,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
|||
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||
${item.provider?.name}
|
||||
</a>`,
|
||||
html`${item.revoked ? t`Yes` : t`No`}`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.scope.join(", ")}`,
|
||||
html`
|
||||
|
|
|
@ -225,7 +225,14 @@ export abstract class Table<T> extends LitElement {
|
|||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`<button
|
||||
@click=${() => { this.fetch(); }}
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}
|
||||
class="pf-c-button pf-m-primary">
|
||||
${t`Refresh`}
|
||||
</button>`;
|
||||
|
@ -241,7 +248,12 @@ export abstract class Table<T> extends LitElement {
|
|||
}
|
||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.fetch();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
</ak-table-search> `;
|
||||
}
|
||||
|
@ -274,7 +286,15 @@ export abstract class Table<T> extends LitElement {
|
|||
<ak-table-pagination
|
||||
class="pf-c-toolbar__item pf-m-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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -300,7 +320,15 @@ export abstract class Table<T> extends LitElement {
|
|||
<ak-table-pagination
|
||||
class="pf-c-toolbar__item pf-m-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>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
@ -488,8 +488,12 @@ msgid "Certificate"
|
|||
msgstr "Certificate"
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Certificate Fingerprint"
|
||||
msgstr "Certificate Fingerprint"
|
||||
msgid "Certificate Fingerprint (SHA1)"
|
||||
msgstr "Certificate Fingerprint (SHA1)"
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Certificate Fingerprint (SHA256)"
|
||||
msgstr "Certificate Fingerprint (SHA256)"
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Certificate Subjet"
|
||||
|
@ -2346,6 +2350,7 @@ msgstr "Negates the outcome of the binding. Messages are unaffected."
|
|||
msgid "New version available!"
|
||||
msgstr "New version available!"
|
||||
|
||||
#: src/elements/oauth/UserRefreshList.ts
|
||||
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
#: src/pages/groups/GroupListPage.ts
|
||||
|
@ -3044,6 +3049,10 @@ msgstr "Return home"
|
|||
msgid "Return to device picker"
|
||||
msgstr "Return to device picker"
|
||||
|
||||
#: src/elements/oauth/UserRefreshList.ts
|
||||
msgid "Revoked?"
|
||||
msgstr "Revoked?"
|
||||
|
||||
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
||||
msgid "SAML Attribute Name"
|
||||
msgstr "SAML Attribute Name"
|
||||
|
@ -4544,6 +4553,7 @@ msgstr ""
|
|||
msgid "X509 Subject"
|
||||
msgstr "X509 Subject"
|
||||
|
||||
#: src/elements/oauth/UserRefreshList.ts
|
||||
#: src/pages/applications/ApplicationCheckAccessForm.ts
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
#: src/pages/groups/GroupListPage.ts
|
||||
|
|
|
@ -484,7 +484,11 @@ msgid "Certificate"
|
|||
msgstr ""
|
||||
|
||||
#:
|
||||
msgid "Certificate Fingerprint"
|
||||
msgid "Certificate Fingerprint (SHA1)"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
msgid "Certificate Fingerprint (SHA256)"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
|
@ -2350,6 +2354,7 @@ msgstr ""
|
|||
#:
|
||||
#:
|
||||
#:
|
||||
#:
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3036,6 +3041,10 @@ msgstr ""
|
|||
msgid "Return to device picker"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
msgid "Revoked?"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
msgid "SAML Attribute Name"
|
||||
msgstr ""
|
||||
|
@ -4539,6 +4548,7 @@ msgstr ""
|
|||
#:
|
||||
#:
|
||||
#:
|
||||
#:
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -103,10 +103,18 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
|||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
|
|
|
@ -15,7 +15,7 @@ export class OutpostHealthElement extends LitElement {
|
|||
outpostId?: string;
|
||||
|
||||
@property({attribute: false})
|
||||
outpostHealth: OutpostHealth[] = [];
|
||||
outpostHealth?: OutpostHealth[];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, AKGlobal];
|
||||
|
@ -23,7 +23,8 @@ export class OutpostHealthElement extends LitElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
window.addEventListener(EVENT_REFRESH, () => {
|
||||
this.outpostHealth = undefined;
|
||||
this.firstUpdated();
|
||||
});
|
||||
}
|
||||
|
@ -38,7 +39,7 @@ export class OutpostHealthElement extends LitElement {
|
|||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.outpostId) {
|
||||
if (!this.outpostId || !this.outpostHealth) {
|
||||
return html`<ak-spinner></ak-spinner>`;
|
||||
}
|
||||
if (this.outpostHealth.length === 0) {
|
||||
|
|
|
@ -9,13 +9,14 @@ import "../../elements/buttons/ActionButton";
|
|||
import { TableColumn } from "../../elements/table/Table";
|
||||
import { PAGE_SIZE } from "../../constants";
|
||||
import { CoreApi, User } from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
|
||||
import "../../elements/forms/DeleteForm";
|
||||
import "./UserActiveForm";
|
||||
import "./UserForm";
|
||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||
import { MessageLevel } from "../../elements/messages/Message";
|
||||
import { first } from "../../utils";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
||||
@customElement("ak-user-list")
|
||||
export class UserListPage extends TablePage<User> {
|
||||
|
@ -128,27 +129,33 @@ export class UserListPage extends TablePage<User> {
|
|||
</li>
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
<ak-action-button
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({
|
||||
id: item.pk || 0,
|
||||
}).then(rec => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: t`Successfully generated recovery link`,
|
||||
description: rec.link
|
||||
});
|
||||
}).catch((ex: Response) => {
|
||||
ex.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`No recovery flow is configured.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}}>
|
||||
${t`Reset Password`}
|
||||
</ak-action-button>
|
||||
${until(tenant().then(te => {
|
||||
if (te.flowRecovery) {
|
||||
return html`
|
||||
<ak-action-button
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({
|
||||
id: item.pk || 0,
|
||||
}).then(rec => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: t`Successfully generated recovery link`,
|
||||
description: rec.link
|
||||
});
|
||||
}).catch((ex: Response) => {
|
||||
ex.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`No recovery flow is configured.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}}>
|
||||
${t`Reset Password`}
|
||||
</ak-action-button>`;
|
||||
}
|
||||
return html``;
|
||||
}))}
|
||||
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
|
||||
${t`Impersonate`}
|
||||
</a>`,
|
||||
|
|
|
@ -6,17 +6,17 @@ This installation method is for test-setups and small-scale productive setups.
|
|||
|
||||
## 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-compose
|
||||
|
||||
## 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 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:
|
||||
|
||||
|
|
|
@ -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`
|
||||
- Audience: `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
|
||||
|
||||
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
|
||||
gitlab_rails['omniauth_enabled'] = true
|
||||
|
@ -46,7 +47,7 @@ gitlab_rails['omniauth_providers'] = [
|
|||
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
||||
# 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_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',
|
||||
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
attribute_statements: {
|
||||
|
|
|
@ -11,7 +11,7 @@ version: "3.5"
|
|||
|
||||
services:
|
||||
authentik_proxy:
|
||||
image: ghcr.io/goauthentik/proxy:2021.6.3
|
||||
image: ghcr.io/goauthentik/proxy:2021.6.4
|
||||
ports:
|
||||
- 4180:4180
|
||||
- 4443:4443
|
||||
|
@ -21,7 +21,7 @@ services:
|
|||
AUTHENTIK_TOKEN: token-generated-by-authentik
|
||||
# Or, for the LDAP Outpost
|
||||
authentik_proxy:
|
||||
image: ghcr.io/goauthentik/ldap:2021.6.3
|
||||
image: ghcr.io/goauthentik/ldap:2021.6.4
|
||||
ports:
|
||||
- 389:3389
|
||||
environment:
|
||||
|
|
|
@ -14,7 +14,7 @@ metadata:
|
|||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
name: authentik-outpost-api
|
||||
stringData:
|
||||
authentik_host: "__AUTHENTIK_URL__"
|
||||
|
@ -29,7 +29,7 @@ metadata:
|
|||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
ports:
|
||||
|
@ -54,7 +54,7 @@ metadata:
|
|||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
selector:
|
||||
|
@ -62,14 +62,14 @@ spec:
|
|||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
|
@ -88,7 +88,7 @@ spec:
|
|||
secretKeyRef:
|
||||
key: authentik_host_insecure
|
||||
name: authentik-outpost-api
|
||||
image: ghcr.io/goauthentik/proxy:2021.6.3
|
||||
image: ghcr.io/goauthentik/proxy:2021.6.4
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
|
@ -110,7 +110,7 @@ metadata:
|
|||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.6.3
|
||||
app.kubernetes.io/version: 2021.6.4
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
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 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
|
||||
|
||||
This release does not introduce any new requirements.
|
||||
|
|
Reference in New Issue