Compare commits
21 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| eaad564e23 | |||
| 511a94975b | |||
| 015810a2fd | |||
| e70e6b84c2 | |||
| d0b9c9a26f | |||
| 3e403fa348 | |||
| 48f4a971ef | |||
| 6314be14ad | |||
| 1a072c6c39 | |||
| ef2eed0bdf | |||
| 91227b1e96 | |||
| 67d68629da | |||
| e875db8f66 | |||
| 055a76393d | |||
| 0754821628 | |||
| fca88d9896 | |||
| dfe0404c51 | |||
| fa61696b46 | |||
| e5773738f4 | |||
| cac8539d79 | |||
| cf600f6f26 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2022.1.4
|
current_version = 2022.1.5
|
||||||
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>.*)
|
||||||
|
|||||||
14
.github/workflows/release-publish.yml
vendored
14
.github/workflows/release-publish.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2022.1.4,
|
beryju/authentik:2022.1.5,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2022.1.4,
|
ghcr.io/goauthentik/server:2022.1.5,
|
||||||
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('2022.1.4', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2022.1.5', '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
|
||||||
@ -78,14 +78,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-${{ matrix.type }}:2022.1.4,
|
beryju/authentik-${{ matrix.type }}:2022.1.5,
|
||||||
beryju/authentik-${{ matrix.type }}:latest,
|
beryju/authentik-${{ matrix.type }}:latest,
|
||||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4,
|
ghcr.io/goauthentik/${{ matrix.type }}:2022.1.5,
|
||||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||||
file: ${{ matrix.type }}.Dockerfile
|
file: ${{ matrix.type }}.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('2022.1.4', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2022.1.5', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-${{ matrix.type }}:latest
|
docker pull beryju/authentik-${{ matrix.type }}:latest
|
||||||
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
||||||
@ -170,7 +170,7 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2022.1.4
|
version: authentik@2022.1.5
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
url_prefix: '~/static/dist'
|
url_prefix: '~/static/dist'
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -12,7 +12,8 @@
|
|||||||
"totp",
|
"totp",
|
||||||
"webauthn",
|
"webauthn",
|
||||||
"traefik",
|
"traefik",
|
||||||
"passwordless"
|
"passwordless",
|
||||||
|
"kubernetes"
|
||||||
],
|
],
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
__version__ = "2022.1.4"
|
__version__ = "2022.1.5"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.functional import SimpleLazyObject
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField, SerializerMethodField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -39,11 +42,22 @@ def user_app_cache_key(user_pk: str) -> str:
|
|||||||
class ApplicationSerializer(ModelSerializer):
|
class ApplicationSerializer(ModelSerializer):
|
||||||
"""Application Serializer"""
|
"""Application Serializer"""
|
||||||
|
|
||||||
launch_url = ReadOnlyField(source="get_launch_url")
|
launch_url = SerializerMethodField()
|
||||||
provider_obj = ProviderSerializer(source="get_provider", required=False)
|
provider_obj = ProviderSerializer(source="get_provider", required=False)
|
||||||
|
|
||||||
meta_icon = ReadOnlyField(source="get_meta_icon")
|
meta_icon = ReadOnlyField(source="get_meta_icon")
|
||||||
|
|
||||||
|
def get_launch_url(self, app: Application) -> Optional[str]:
|
||||||
|
"""Allow formatting of launch URL"""
|
||||||
|
url = app.get_launch_url()
|
||||||
|
if not url:
|
||||||
|
return url
|
||||||
|
user = self.context["request"].user
|
||||||
|
if isinstance(user, SimpleLazyObject):
|
||||||
|
user._setup()
|
||||||
|
user = user._wrapped
|
||||||
|
return url % user.__dict__
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Application
|
model = Application
|
||||||
|
|||||||
@ -13,7 +13,9 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.allowed = Application.objects.create(name="allowed", slug="allowed")
|
self.allowed = Application.objects.create(
|
||||||
|
name="allowed", slug="allowed", meta_launch_url="https://goauthentik.io/%(username)s"
|
||||||
|
)
|
||||||
self.denied = Application.objects.create(name="denied", slug="denied")
|
self.denied = Application.objects.create(name="denied", slug="denied")
|
||||||
PolicyBinding.objects.create(
|
PolicyBinding.objects.create(
|
||||||
target=self.denied,
|
target=self.denied,
|
||||||
@ -64,8 +66,8 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"slug": "allowed",
|
"slug": "allowed",
|
||||||
"provider": None,
|
"provider": None,
|
||||||
"provider_obj": None,
|
"provider_obj": None,
|
||||||
"launch_url": None,
|
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||||
"meta_launch_url": "",
|
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||||
"meta_icon": None,
|
"meta_icon": None,
|
||||||
"meta_description": "",
|
"meta_description": "",
|
||||||
"meta_publisher": "",
|
"meta_publisher": "",
|
||||||
@ -100,8 +102,8 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"slug": "allowed",
|
"slug": "allowed",
|
||||||
"provider": None,
|
"provider": None,
|
||||||
"provider_obj": None,
|
"provider_obj": None,
|
||||||
"launch_url": None,
|
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||||
"meta_launch_url": "",
|
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||||
"meta_icon": None,
|
"meta_icon": None,
|
||||||
"meta_description": "",
|
"meta_description": "",
|
||||||
"meta_publisher": "",
|
"meta_publisher": "",
|
||||||
|
|||||||
@ -55,6 +55,10 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
|
|
||||||
first_msg = False
|
first_msg = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.logger = get_logger()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
super().connect()
|
super().connect()
|
||||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||||
@ -65,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
)
|
)
|
||||||
if not outpost:
|
if not outpost:
|
||||||
raise DenyConnection()
|
raise DenyConnection()
|
||||||
self.logger = get_logger().bind(outpost=outpost)
|
self.logger = self.logger.bind(outpost=outpost)
|
||||||
try:
|
try:
|
||||||
self.accept()
|
self.accept()
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from kubernetes.client.models.v1_container_port import V1ContainerPort
|
from kubernetes.client.models.v1_container_port import V1ContainerPort
|
||||||
|
from kubernetes.client.models.v1_service_port import V1ServicePort
|
||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||||
|
|
||||||
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
|
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
|
||||||
@ -16,10 +17,31 @@ def get_namespace() -> str:
|
|||||||
return "default"
|
return "default"
|
||||||
|
|
||||||
|
|
||||||
def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]):
|
def compare_port(
|
||||||
|
current: V1ServicePort | V1ContainerPort, reference: V1ServicePort | V1ContainerPort
|
||||||
|
) -> bool:
|
||||||
|
"""Compare a single port"""
|
||||||
|
if current.name != reference.name:
|
||||||
|
return False
|
||||||
|
if current.protocol != reference.protocol:
|
||||||
|
return False
|
||||||
|
if isinstance(current, V1ServicePort) and isinstance(reference, V1ServicePort):
|
||||||
|
# We only care about the target port
|
||||||
|
if current.target_port != reference.target_port:
|
||||||
|
return False
|
||||||
|
if isinstance(current, V1ContainerPort) and isinstance(reference, V1ContainerPort):
|
||||||
|
# We only care about the target port
|
||||||
|
if current.container_port != reference.container_port:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def compare_ports(
|
||||||
|
current: list[V1ServicePort | V1ContainerPort], reference: list[V1ServicePort | V1ContainerPort]
|
||||||
|
):
|
||||||
"""Compare ports of a list"""
|
"""Compare ports of a list"""
|
||||||
if len(current) != len(reference):
|
if len(current) != len(reference):
|
||||||
raise NeedsRecreate()
|
raise NeedsRecreate()
|
||||||
for port in reference:
|
for port in reference:
|
||||||
if port not in current:
|
if not any(compare_port(port, current_port) for current_port in current):
|
||||||
raise NeedsRecreate()
|
raise NeedsRecreate()
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from authentik.providers.saml.processors.request_parser import AuthNRequestParse
|
|||||||
from authentik.sources.saml.exceptions import MismatchedRequestID
|
from authentik.sources.saml.exceptions import MismatchedRequestID
|
||||||
from authentik.sources.saml.models import SAMLSource
|
from authentik.sources.saml.models import SAMLSource
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
|
SAML_BINDING_REDIRECT,
|
||||||
SAML_NAME_ID_FORMAT_EMAIL,
|
SAML_NAME_ID_FORMAT_EMAIL,
|
||||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||||
)
|
)
|
||||||
@ -98,6 +99,9 @@ class TestAuthNRequest(TestCase):
|
|||||||
|
|
||||||
# First create an AuthNRequest
|
# First create an AuthNRequest
|
||||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||||
|
auth_n = request_proc.get_auth_n()
|
||||||
|
self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_REDIRECT)
|
||||||
|
|
||||||
request = request_proc.build_auth_n()
|
request = request_proc.build_auth_n()
|
||||||
# Now we check the ID and signature
|
# Now we check the ID and signature
|
||||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from ldap3.core.exceptions import LDAPException
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
@ -52,5 +53,5 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
|
|||||||
)
|
)
|
||||||
except LDAPException as exc:
|
except LDAPException as exc:
|
||||||
# No explicit event is created here as .set_status with an error will do that
|
# No explicit event is created here as .set_status with an error will do that
|
||||||
LOGGER.debug(exc)
|
LOGGER.warning(exception_to_string(exc))
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||||
|
|||||||
@ -18,6 +18,8 @@ from authentik.sources.saml.processors.constants import (
|
|||||||
RSA_SHA256,
|
RSA_SHA256,
|
||||||
RSA_SHA384,
|
RSA_SHA384,
|
||||||
RSA_SHA512,
|
RSA_SHA512,
|
||||||
|
SAML_BINDING_POST,
|
||||||
|
SAML_BINDING_REDIRECT,
|
||||||
SAML_NAME_ID_FORMAT_EMAIL,
|
SAML_NAME_ID_FORMAT_EMAIL,
|
||||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||||
@ -37,6 +39,15 @@ class SAMLBindingTypes(models.TextChoices):
|
|||||||
POST = "POST", _("POST Binding")
|
POST = "POST", _("POST Binding")
|
||||||
POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation")
|
POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uri(self) -> str:
|
||||||
|
"""Convert database field to URI"""
|
||||||
|
return {
|
||||||
|
SAMLBindingTypes.POST: SAML_BINDING_POST,
|
||||||
|
SAMLBindingTypes.POST_AUTO: SAML_BINDING_POST,
|
||||||
|
SAMLBindingTypes.REDIRECT: SAML_BINDING_REDIRECT,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
|
||||||
class SAMLNameIDPolicy(models.TextChoices):
|
class SAMLNameIDPolicy(models.TextChoices):
|
||||||
"""SAML NameID Policies"""
|
"""SAML NameID Policies"""
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from lxml.etree import Element # nosec
|
|||||||
from authentik.providers.saml.utils import get_random_id
|
from authentik.providers.saml.utils import get_random_id
|
||||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
|
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||||
from authentik.providers.saml.utils.time import get_time_string
|
from authentik.providers.saml.utils.time import get_time_string
|
||||||
from authentik.sources.saml.models import SAMLSource
|
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||||
NS_MAP,
|
NS_MAP,
|
||||||
@ -62,7 +62,7 @@ class RequestProcessor:
|
|||||||
auth_n_request.attrib["Destination"] = self.source.sso_url
|
auth_n_request.attrib["Destination"] = self.source.sso_url
|
||||||
auth_n_request.attrib["ID"] = self.request_id
|
auth_n_request.attrib["ID"] = self.request_id
|
||||||
auth_n_request.attrib["IssueInstant"] = self.issue_instant
|
auth_n_request.attrib["IssueInstant"] = self.issue_instant
|
||||||
auth_n_request.attrib["ProtocolBinding"] = self.source.binding_type
|
auth_n_request.attrib["ProtocolBinding"] = SAMLBindingTypes(self.source.binding_type).uri
|
||||||
auth_n_request.attrib["Version"] = "2.0"
|
auth_n_request.attrib["Version"] = "2.0"
|
||||||
# Create issuer object
|
# Create issuer object
|
||||||
auth_n_request.append(self.get_issuer())
|
auth_n_request.append(self.get_issuer())
|
||||||
|
|||||||
@ -196,7 +196,10 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
||||||
challenges = self.request.session["device_challenges"]
|
challenges = self.request.session.get("device_challenges")
|
||||||
|
if not challenges:
|
||||||
|
LOGGER.debug("Authenticator Validation stage ran without challenges")
|
||||||
|
return self.executor.stage_invalid()
|
||||||
return AuthenticatorValidationChallenge(
|
return AuthenticatorValidationChallenge(
|
||||||
data={
|
data={
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
|
|||||||
@ -17,7 +17,7 @@ services:
|
|||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -38,7 +38,7 @@ services:
|
|||||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@ -25,4 +25,4 @@ func OutpostUserAgent() string {
|
|||||||
return fmt.Sprintf("authentik-outpost@%s", FullVersion())
|
return fmt.Sprintf("authentik-outpost@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2022.1.4"
|
const VERSION = "2022.1.5"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type Claims struct {
|
|||||||
Exp int `json:"exp"`
|
Exp int `json:"exp"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Verified bool `json:"email_verified"`
|
Verified bool `json:"email_verified"`
|
||||||
Proxy ProxyClaims `json:"ak_proxy"`
|
Proxy *ProxyClaims `json:"ak_proxy"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
PreferredUsername string `json:"preferred_username"`
|
PreferredUsername string `json:"preferred_username"`
|
||||||
Groups []string `json:"groups"`
|
Groups []string `json:"groups"`
|
||||||
|
|||||||
@ -70,7 +70,7 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
|||||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
UserAttributes: map[string]interface{}{
|
UserAttributes: map[string]interface{}{
|
||||||
"username": "foo",
|
"username": "foo",
|
||||||
"password": "bar",
|
"password": "bar",
|
||||||
|
|||||||
@ -64,7 +64,7 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
|||||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
UserAttributes: map[string]interface{}{
|
UserAttributes: map[string]interface{}{
|
||||||
"username": "foo",
|
"username": "foo",
|
||||||
"password": "bar",
|
"password": "bar",
|
||||||
|
|||||||
@ -74,18 +74,21 @@ func (a *Application) configureProxy() error {
|
|||||||
func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
|
func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
|
||||||
return func(r *http.Request) {
|
return func(r *http.Request) {
|
||||||
claims, _ := a.getClaims(r)
|
claims, _ := a.getClaims(r)
|
||||||
if claims.Proxy.BackendOverride != "" {
|
r.URL.Scheme = ou.Scheme
|
||||||
|
r.URL.Host = ou.Host
|
||||||
|
r.Host = ou.Host
|
||||||
|
if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
|
||||||
u, err := url.Parse(claims.Proxy.BackendOverride)
|
u, err := url.Parse(claims.Proxy.BackendOverride)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
||||||
}
|
} else {
|
||||||
r.URL.Scheme = u.Scheme
|
r.URL.Scheme = u.Scheme
|
||||||
r.URL.Host = u.Host
|
r.URL.Host = u.Host
|
||||||
} else {
|
r.Host = u.Host
|
||||||
r.URL.Scheme = ou.Scheme
|
|
||||||
r.URL.Host = ou.Host
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) proxyModifyResponse(res *http.Response) error {
|
func (a *Application) proxyModifyResponse(res *http.Response) error {
|
||||||
|
|||||||
81
internal/outpost/proxyv2/application/mode_proxy_test.go
Normal file
81
internal/outpost/proxyv2/application/mode_proxy_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProxy_ModifyRequest(t *testing.T) {
|
||||||
|
a := newTestApplication()
|
||||||
|
req, _ := http.NewRequest("GET", "http://frontend/foo", nil)
|
||||||
|
u, err := url.Parse("http://backend:8012")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
a.proxyModifyRequest(u)(req)
|
||||||
|
|
||||||
|
assert.Equal(t, "/foo", req.URL.Path)
|
||||||
|
assert.Equal(t, "backend:8012", req.URL.Host)
|
||||||
|
assert.Equal(t, "backend:8012", req.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxy_ModifyRequest_Claims(t *testing.T) {
|
||||||
|
a := newTestApplication()
|
||||||
|
req, _ := http.NewRequest("GET", "http://frontend/foo", nil)
|
||||||
|
u, err := url.Parse("http://backend:8012")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||||
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
|
Sub: "foo",
|
||||||
|
Proxy: &ProxyClaims{
|
||||||
|
BackendOverride: "http://other-backend:8123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = a.sessions.Save(req, rr, s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.proxyModifyRequest(u)(req)
|
||||||
|
|
||||||
|
assert.Equal(t, "/foo", req.URL.Path)
|
||||||
|
assert.Equal(t, "other-backend:8123", req.URL.Host)
|
||||||
|
assert.Equal(t, "other-backend:8123", req.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) {
|
||||||
|
a := newTestApplication()
|
||||||
|
req, _ := http.NewRequest("GET", "http://frontend/foo", nil)
|
||||||
|
u, err := url.Parse("http://backend:8012")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||||
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
|
Sub: "foo",
|
||||||
|
Proxy: &ProxyClaims{
|
||||||
|
BackendOverride: ":qewr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = a.sessions.Save(req, rr, s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.proxyModifyRequest(u)(req)
|
||||||
|
|
||||||
|
assert.Equal(t, "/foo", req.URL.Path)
|
||||||
|
assert.Equal(t, "backend:8012", req.URL.Host)
|
||||||
|
assert.Equal(t, "backend:8012", req.Host)
|
||||||
|
}
|
||||||
@ -102,7 +102,11 @@ func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
appCert := ps.GetCertificate(info.ServerName)
|
sn := info.ServerName
|
||||||
|
if sn == "" {
|
||||||
|
return &ps.defaultCert, nil
|
||||||
|
}
|
||||||
|
appCert := ps.GetCertificate(sn)
|
||||||
if appCert == nil {
|
if appCert == nil {
|
||||||
return &ps.defaultCert, nil
|
return &ps.defaultCert, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -24,6 +25,7 @@ func (ws *WebServer) configureProxy() {
|
|||||||
if req.TLS != nil {
|
if req.TLS != nil {
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
}
|
}
|
||||||
|
ws.log.WithField("url", req.URL.String()).WithField("headers", req.Header).Trace("tracing request to backend")
|
||||||
}
|
}
|
||||||
rp := &httputil.ReverseProxy{Director: director}
|
rp := &httputil.ReverseProxy{Director: director}
|
||||||
rp.ErrorHandler = ws.proxyErrorHandler
|
rp.ErrorHandler = ws.proxyErrorHandler
|
||||||
@ -65,9 +67,20 @@ func (ws *WebServer) configureProxy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
||||||
ws.log.Warning(err.Error())
|
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||||
rw.WriteHeader(http.StatusBadGateway)
|
rw.WriteHeader(http.StatusBadGateway)
|
||||||
_, err = rw.Write([]byte("authentik starting..."))
|
em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
|
||||||
|
if !ws.p.IsRunning() {
|
||||||
|
em = "authentik starting..."
|
||||||
|
}
|
||||||
|
// return json if the client asks for json
|
||||||
|
if req.Header.Get("Accept") == "application/json" {
|
||||||
|
eem, _ := json.Marshal(map[string]string{
|
||||||
|
"error": em,
|
||||||
|
})
|
||||||
|
em = string(eem)
|
||||||
|
}
|
||||||
|
_, err = rw.Write([]byte(em))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.log.WithError(err).Warning("failed to write error message")
|
ws.log.WithError(err).Warning("failed to write error message")
|
||||||
}
|
}
|
||||||
@ -75,5 +88,6 @@ func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request
|
|||||||
|
|
||||||
func (ws *WebServer) proxyModifyResponse(r *http.Response) error {
|
func (ws *WebServer) proxyModifyResponse(r *http.Response) error {
|
||||||
r.Header.Set("X-Powered-By", "authentik")
|
r.Header.Set("X-Powered-By", "authentik")
|
||||||
|
r.Header.Del("Server")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,9 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Certif
|
|||||||
ws.log.WithError(err).Error("failed to generate default cert")
|
ws.log.WithError(err).Error("failed to generate default cert")
|
||||||
}
|
}
|
||||||
return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if ch.ServerName == "" {
|
||||||
|
return &cert, nil
|
||||||
|
}
|
||||||
if ws.ProxyServer != nil {
|
if ws.ProxyServer != nil {
|
||||||
appCert := ws.ProxyServer.GetCertificate(ch.ServerName)
|
appCert := ws.ProxyServer.GetCertificate(ch.ServerName)
|
||||||
if appCert != nil {
|
if appCert != nil {
|
||||||
|
|||||||
6
poetry.lock
generated
6
poetry.lock
generated
@ -1898,7 +1898,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.17.1"
|
version = "0.17.3"
|
||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -3406,8 +3406,8 @@ urllib3 = [
|
|||||||
{file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"},
|
{file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"},
|
||||||
]
|
]
|
||||||
uvicorn = [
|
uvicorn = [
|
||||||
{file = "uvicorn-0.17.1-py3-none-any.whl", hash = "sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73"},
|
{file = "uvicorn-0.17.3-py3-none-any.whl", hash = "sha256:3ab1bf48aa512692db93a91c514576a0739a9d3522827e1656a172ee87118fa5"},
|
||||||
{file = "uvicorn-0.17.1.tar.gz", hash = "sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41"},
|
{file = "uvicorn-0.17.3.tar.gz", hash = "sha256:3cebddac78c7dd6bfce2f8f838c2c9b0cfcf3dddf417315ba62378ebe7e8fae2"},
|
||||||
]
|
]
|
||||||
uvloop = [
|
uvloop = [
|
||||||
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
|
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
|
||||||
|
|||||||
@ -92,7 +92,7 @@ addopts = "-p no:celery --junitxml=unittest.xml"
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2022.1.4"
|
version = "2022.1.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Jens Langhammer <jens.langhammer@beryju.org>"]
|
authors = ["Jens Langhammer <jens.langhammer@beryju.org>"]
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2022.1.4
|
version: 2022.1.5
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@beryju.org
|
email: hello@beryju.org
|
||||||
|
|||||||
@ -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 = "2022.1.4";
|
export const VERSION = "2022.1.5";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export class AdminOverviewPage extends LitElement {
|
|||||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="pf-icon pf-icon-server"
|
icon="pf-icon pf-icon-process-automation"
|
||||||
header=${t`Flows`}
|
header=${t`Flows`}
|
||||||
headerLink="#/flow/flows"
|
headerLink="#/flow/flows"
|
||||||
>
|
>
|
||||||
@ -121,7 +121,7 @@ export class AdminOverviewPage extends LitElement {
|
|||||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="fa fa-sync-alt"
|
icon="pf-icon pf-icon-zone"
|
||||||
header=${t`Outpost status`}
|
header=${t`Outpost status`}
|
||||||
headerLink="#/outpost/outposts"
|
headerLink="#/outpost/outposts"
|
||||||
>
|
>
|
||||||
@ -132,7 +132,7 @@ export class AdminOverviewPage extends LitElement {
|
|||||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="fa fa-sync-alt"
|
icon="pf-icon pf-icon-user"
|
||||||
header=${t`Users`}
|
header=${t`Users`}
|
||||||
headerLink="#/identity/users"
|
headerLink="#/identity/users"
|
||||||
>
|
>
|
||||||
@ -143,7 +143,7 @@ export class AdminOverviewPage extends LitElement {
|
|||||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="fa fa-sync-alt"
|
icon="pf-icon pf-icon-users"
|
||||||
header=${t`Groups`}
|
header=${t`Groups`}
|
||||||
headerLink="#/identity/groups"
|
headerLink="#/identity/groups"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -24,6 +24,9 @@ The following aspects can be configured:
|
|||||||
|
|
||||||
- *Name*: This is the name shown for the application card
|
- *Name*: This is the name shown for the application card
|
||||||
- *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider
|
- *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider
|
||||||
|
|
||||||
|
Starting with authentik 2022.2, you can use placeholders in the launch url to build them dynamically based on logged in user. For example, you can set the Launch URL to `https://goauthentik.io/%(username)s`, which will be replaced with the currently logged in user's username.
|
||||||
|
|
||||||
- *Icon (URL)*: Optionally configure an Icon for the application
|
- *Icon (URL)*: Optionally configure an Icon for the application
|
||||||
- *Publisher*: Text shown below the application
|
- *Publisher*: Text shown below the application
|
||||||
- *Description*: Subtext shown on the application card below the publisher
|
- *Description*: Subtext shown on the application card below the publisher
|
||||||
|
|||||||
@ -102,6 +102,28 @@ This release mostly removes legacy fields and features that have been deprecated
|
|||||||
- web/flows: fix width on flow container
|
- web/flows: fix width on flow container
|
||||||
- web/user: include locale code in locale selection
|
- web/user: include locale code in locale selection
|
||||||
|
|
||||||
|
## Fixed in 2022.1.5
|
||||||
|
|
||||||
|
- build(deps): bump uvicorn from 0.17.1 to 0.17.3 (#2229)
|
||||||
|
- core: allow formatting strings to be used for applications' launch URLs
|
||||||
|
- internal: don't attempt to lookup SNI Certificate if no SNI is sent
|
||||||
|
- internal: fix CSRF error caused by Host header
|
||||||
|
- internal: improve error handling for internal reverse proxy
|
||||||
|
- internal: remove uvicorn server header
|
||||||
|
- internal: trace headers and url for backend requests
|
||||||
|
- outposts: fix channel not always having a logger attribute
|
||||||
|
- outposts: fix compare_ports to support both service and container ports
|
||||||
|
- outposts: fix service reconciler re-creating services
|
||||||
|
- outposts: remove node_port on V1ServicePort checks to prevent service creation loops
|
||||||
|
- providers/proxy: fix Host/:Authority not being modified
|
||||||
|
- providers/proxy: fix nil error in claims
|
||||||
|
- providers/proxy: improve error handling for invalid backend_override
|
||||||
|
- sources/ldap: log entire exception
|
||||||
|
- sources/saml: fix incorrect ProtocolBinding being sent
|
||||||
|
- sources/saml: fix server error
|
||||||
|
- stages/authenticator_validate: handle non-existent device_challenges
|
||||||
|
- web/admin: fix mismatched icons in overview and lists
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
This release does not introduce any new requirements.
|
This release does not introduce any new requirements.
|
||||||
|
|||||||
Reference in New Issue
Block a user