Compare commits
171 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
4a1acd377b | |||
c5b84a91d1 | |||
e77ecda3b8 | |||
4e317c10c5 | |||
eb05a3ddb8 | |||
a22d6a0924 | |||
3f0d67779a | |||
0a937ae8e9 | |||
f8d94f3039 | |||
6bb261ac62 | |||
45f2c5bae7 | |||
5d8c1aa0b0 | |||
0101368369 | |||
4854f81592 | |||
4bed6e02e5 | |||
908f123d0e | |||
256dd24a1e | |||
d4284407f9 | |||
80da5dfc52 | |||
b6edf990e0 | |||
a66dcf9382 | |||
9095a840d5 | |||
72259f6479 | |||
0973c74b9d | |||
c7ed4f7ac1 | |||
3d577cf15e | |||
5474a32573 | |||
a5940b88e3 | |||
ff15716012 | |||
c040b13b29 | |||
4915e980c5 | |||
df362dd9ea | |||
d4e4f93cb4 | |||
3af0de6a00 | |||
4f24d61290 | |||
4c5c4dcf2c | |||
660b5cb6c6 | |||
6ff1ea73a9 | |||
3de224690a | |||
d4624b510a | |||
8856d762d0 | |||
5d1cbf14d1 | |||
6d5207f644 | |||
3b6497cd51 | |||
ff7320b0f8 | |||
e5a393c534 | |||
bb4be944dc | |||
21efee8f44 | |||
f61549a60f | |||
0a7bafd1b2 | |||
b3987c5fa0 | |||
0da043a9fe | |||
f336f204cb | |||
3bfcf18492 | |||
dfafe8b43d | |||
b5d43b15f8 | |||
2ccab75021 | |||
9070df6c26 | |||
a1c8ad55ad | |||
872c05c690 | |||
a9528dc1b5 | |||
0e59ade1f2 | |||
5ac49c695d | |||
3a30ecbe76 | |||
1f838bb2aa | |||
cc42830e23 | |||
593eb959ca | |||
5bb6785ad6 | |||
535c11a729 | |||
a0fa8d8524 | |||
c14025c579 | |||
8bc3db7c90 | |||
eaad564e23 | |||
511a94975b | |||
015810a2fd | |||
e70e6b84c2 | |||
d0b9c9a26f | |||
3e403fa348 | |||
48f4a971ef | |||
6314be14ad | |||
1a072c6c39 | |||
ef2eed0bdf | |||
91227b1e96 | |||
67d68629da | |||
e875db8f66 | |||
055a76393d | |||
0754821628 | |||
fca88d9896 | |||
dfe0404c51 | |||
fa61696b46 | |||
e5773738f4 | |||
cac8539d79 | |||
cf600f6f26 | |||
e194715c3e | |||
787f02d5dc | |||
a0ed01a610 | |||
02ba493759 | |||
a7fea5434d | |||
4fb783e953 | |||
affbf85699 | |||
0d92112a3f | |||
b1ad3ec9db | |||
c0601baca6 | |||
057c5c5e9a | |||
05429ab848 | |||
b66d51a699 | |||
f834bc0ff2 | |||
93fd883d7a | |||
7e080d4d68 | |||
3e3ca22d04 | |||
e741caa6b3 | |||
4343246a41 | |||
3f6f83b4b6 | |||
c63e1c9b87 | |||
f44cf06d22 | |||
3f609b8601 | |||
edd89b44a4 | |||
3e58748862 | |||
7088a6b0e6 | |||
6c880e0e62 | |||
cb1e70be7f | |||
6ba150f737 | |||
131769ea73 | |||
e68adbb30d | |||
f1eef09099 | |||
5ab3c7fa9f | |||
d0cec39a0f | |||
e15f53a39a | |||
25fb995663 | |||
eac658c64f | |||
15e2032493 | |||
c87f6cd9d9 | |||
e758995458 | |||
20c284a188 | |||
b0936ea8f3 | |||
bfc0f4a413 | |||
1a9a90cf6a | |||
00f1a6fa48 | |||
33754a06d2 | |||
69b838e1cf | |||
d5e04a2301 | |||
fbf251280f | |||
eaadf62f01 | |||
8c33e7a7c1 | |||
a7d9a80a28 | |||
2ea5dce8d3 | |||
14bf01efe4 | |||
67b24a60e4 | |||
e6775297cb | |||
4e4e2b36b6 | |||
3189c56fc3 | |||
5b5ea47b7a | |||
caa382f898 | |||
2d63488197 | |||
c1c8e4c8d4 | |||
a0e451c5e5 | |||
eaba8006e6 | |||
39ff202f8c | |||
654e0d6245 | |||
ec04443493 | |||
d247c262af | |||
dff49b2bef | |||
50666a76fb | |||
b51a7f9746 | |||
001dfd9f6c | |||
5e4fbeeb25 | |||
2c910bf6ca | |||
9b11319e81 | |||
40dc4b3fb8 | |||
0e37b98968 | |||
7e132eb014 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2022.1.4
|
||||
current_version = 2022.2.1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
3
.github/workflows/ci-main.yml
vendored
3
.github/workflows/ci-main.yml
vendored
@ -86,10 +86,9 @@ jobs:
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
cp -R scripts ..
|
||||
cp -R poetry.lock pyproject.toml ..
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts ../poetry.lock ../pyproject.toml .
|
||||
mv ../.github ../scripts .
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
|
14
.github/workflows/release-publish.yml
vendored
14
.github/workflows/release-publish.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2022.1.4,
|
||||
beryju/authentik:2022.2.1,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2022.1.4,
|
||||
ghcr.io/goauthentik/server:2022.2.1,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.2.1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -78,14 +78,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-${{ matrix.type }}:2022.1.4,
|
||||
beryju/authentik-${{ matrix.type }}:2022.2.1,
|
||||
beryju/authentik-${{ matrix.type }}:latest,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.2.1,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.2.1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-${{ matrix.type }}:latest
|
||||
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
||||
@ -170,7 +170,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2022.1.4
|
||||
version: authentik@2022.2.1
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
docker-compose run -u root server test
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -12,7 +12,8 @@
|
||||
"totp",
|
||||
"webauthn",
|
||||
"traefik",
|
||||
"passwordless"
|
||||
"passwordless",
|
||||
"kubernetes"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
|
@ -16,7 +16,7 @@ ENV NODE_ENV=production
|
||||
RUN cd /work/web && npm i && npm run build
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.7-bullseye AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2021.10.x | :white_check_mark: |
|
||||
| 2021.12.x | :white_check_mark: |
|
||||
| 2022.1.x | :white_check_mark: |
|
||||
| 2022.2.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.1.4"
|
||||
__version__ = "2022.2.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -12,10 +12,13 @@ from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TaskSerializer(PassiveSerializer):
|
||||
"""Serialize TaskInfo and TaskResult"""
|
||||
@ -89,6 +92,7 @@ class TaskViewSet(ViewSet):
|
||||
try:
|
||||
task_module = import_module(task.task_call_module)
|
||||
task_func = getattr(task_module, task.task_call_func)
|
||||
LOGGER.debug("Running task", task=task_func)
|
||||
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
|
||||
messages.success(
|
||||
self.request,
|
||||
@ -96,6 +100,7 @@ class TaskViewSet(ViewSet):
|
||||
)
|
||||
return Response(status=204)
|
||||
except (ImportError, AttributeError): # pragma: no cover
|
||||
LOGGER.warning("Failed to run task, remove state", task=task)
|
||||
# if we get an import error, the module path has probably changed
|
||||
task.delete()
|
||||
return Response(status=500)
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""core Configs API"""
|
||||
from os import environ, path
|
||||
from os import path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
@ -28,7 +27,6 @@ class Capabilities(models.TextChoices):
|
||||
|
||||
CAN_SAVE_MEDIA = "can_save_media"
|
||||
CAN_GEO_IP = "can_geo_ip"
|
||||
CAN_BACKUP = "can_backup"
|
||||
|
||||
|
||||
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||
@ -65,13 +63,6 @@ class ConfigView(APIView):
|
||||
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
||||
if GEOIP_READER.enabled:
|
||||
caps.append(Capabilities.CAN_GEO_IP)
|
||||
if SERVICE_HOST_ENV_NAME in environ:
|
||||
# Running in k8s, only s3 backup is supported
|
||||
if CONFIG.y("postgresql.s3_backup"):
|
||||
caps.append(Capabilities.CAN_BACKUP)
|
||||
else:
|
||||
# Running in compose, backup is always supported
|
||||
caps.append(Capabilities.CAN_BACKUP)
|
||||
return caps
|
||||
|
||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||
|
@ -1,13 +1,16 @@
|
||||
"""Application API Views"""
|
||||
from typing import Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import QuerySet
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
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.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -39,11 +42,26 @@ def user_app_cache_key(user_pk: str) -> str:
|
||||
class ApplicationSerializer(ModelSerializer):
|
||||
"""Application Serializer"""
|
||||
|
||||
launch_url = ReadOnlyField(source="get_launch_url")
|
||||
launch_url = SerializerMethodField()
|
||||
provider_obj = ProviderSerializer(source="get_provider", required=False)
|
||||
|
||||
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
|
||||
try:
|
||||
return url % user.__dict__
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("Failed to format launch url", exc=exc)
|
||||
return url
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Application
|
||||
|
@ -1,17 +1,7 @@
|
||||
"""authentik core tasks"""
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from os import environ
|
||||
|
||||
from boto3.exceptions import Boto3Error
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from dbbackup.db.exceptions import CommandConnectorError
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core import management
|
||||
from django.core.cache import cache
|
||||
from django.utils.timezone import now
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, ExpiringModel
|
||||
@ -21,7 +11,6 @@ from authentik.events.monitored_tasks import (
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -53,46 +42,3 @@ def clean_expired_models(self: MonitoredTask):
|
||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||
|
||||
|
||||
def should_backup() -> bool:
|
||||
"""Check if we should be doing backups"""
|
||||
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
|
||||
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
||||
return False
|
||||
if not CONFIG.y_bool("postgresql.backup.enabled"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
self.result_timeout_hours = 25
|
||||
if not should_backup():
|
||||
self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
|
||||
return
|
||||
try:
|
||||
start = datetime.now()
|
||||
out = StringIO()
|
||||
management.call_command("dbbackup", quiet=True, stdout=out)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[
|
||||
f"Successfully finished database backup {naturaltime(start)} {out.getvalue()}",
|
||||
],
|
||||
)
|
||||
)
|
||||
LOGGER.info("Successfully backed up database.")
|
||||
except (
|
||||
IOError,
|
||||
BotoCoreError,
|
||||
ClientError,
|
||||
Boto3Error,
|
||||
PermissionError,
|
||||
CommandConnectorError,
|
||||
ValueError,
|
||||
) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
|
@ -16,6 +16,7 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}">
|
||||
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
|
@ -13,7 +13,9 @@ class TestApplicationsAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
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")
|
||||
PolicyBinding.objects.create(
|
||||
target=self.denied,
|
||||
@ -64,8 +66,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"slug": "allowed",
|
||||
"provider": None,
|
||||
"provider_obj": None,
|
||||
"launch_url": None,
|
||||
"meta_launch_url": "",
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"meta_icon": None,
|
||||
"meta_description": "",
|
||||
"meta_publisher": "",
|
||||
@ -100,8 +102,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"slug": "allowed",
|
||||
"provider": None,
|
||||
"provider_obj": None,
|
||||
"launch_url": None,
|
||||
"meta_launch_url": "",
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"meta_icon": None,
|
||||
"meta_description": "",
|
||||
"meta_publisher": "",
|
||||
|
@ -1,7 +1,5 @@
|
||||
"""events GeoIP Reader"""
|
||||
from datetime import datetime
|
||||
from os import stat
|
||||
from time import time
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
from geoip2.database import Reader
|
||||
@ -46,14 +44,18 @@ class GeoIPReader:
|
||||
LOGGER.warning("Failed to load GeoIP database", exc=exc)
|
||||
|
||||
def __check_expired(self):
|
||||
"""Check if the geoip database has been opened longer than 8 hours,
|
||||
and re-open it, as it will probably will have been re-downloaded"""
|
||||
now = time()
|
||||
diff = datetime.fromtimestamp(now) - datetime.fromtimestamp(self.__last_mtime)
|
||||
diff_hours = diff.total_seconds() // 3600
|
||||
if diff_hours >= 8:
|
||||
LOGGER.info("GeoIP databased loaded too long, re-opening", diff=diff)
|
||||
self.__open()
|
||||
"""Check if the modification date of the GeoIP database has
|
||||
changed, and reload it if so"""
|
||||
path = CONFIG.y("geoip")
|
||||
try:
|
||||
mtime = stat(path).st_mtime
|
||||
diff = self.__last_mtime < mtime
|
||||
if diff > 0:
|
||||
LOGGER.info("Found new GeoIP Database, reopening", diff=diff)
|
||||
self.__open()
|
||||
except OSError as exc:
|
||||
LOGGER.warning("Failed to check GeoIP age", exc=exc)
|
||||
return
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
|
@ -5,16 +5,6 @@ postgresql:
|
||||
user: authentik
|
||||
port: 5432
|
||||
password: 'env://POSTGRES_PASSWORD'
|
||||
backup:
|
||||
enabled: false
|
||||
s3_backup:
|
||||
access_key: ""
|
||||
secret_key: ""
|
||||
bucket: ""
|
||||
region: eu-central-1
|
||||
host: ""
|
||||
location: ""
|
||||
insecure_skip_verify: false
|
||||
|
||||
web:
|
||||
listen: 0.0.0.0:9000
|
||||
@ -65,6 +55,7 @@ outposts:
|
||||
# %(version)s: Current version; 2021.4.1
|
||||
# %(build_hash)s: Build hash if you're running a beta version
|
||||
container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
|
||||
discover: true
|
||||
|
||||
cookie_domain: null
|
||||
disable_update_check: false
|
||||
|
6
authentik/lib/merge.py
Normal file
6
authentik/lib/merge.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""merge utils"""
|
||||
from deepmerge import Merger
|
||||
|
||||
MERGE_LIST_UNIQUE = Merger(
|
||||
[(list, ["append_unique"]), (dict, ["merge"]), (set, ["union"])], ["override"], ["override"]
|
||||
)
|
@ -3,8 +3,6 @@ from typing import Optional
|
||||
|
||||
from aioredis.errors import ConnectionClosedError, ReplyError
|
||||
from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError
|
||||
from botocore.client import ClientError
|
||||
from botocore.exceptions import BotoCoreError
|
||||
from celery.exceptions import CeleryError
|
||||
from channels.middleware import BaseMiddleware
|
||||
from channels_redis.core import ChannelFull
|
||||
@ -81,9 +79,6 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
WorkerLostError,
|
||||
CeleryError,
|
||||
SoftTimeLimitExceeded,
|
||||
# S3 errors
|
||||
BotoCoreError,
|
||||
ClientError,
|
||||
# custom baseclass
|
||||
SentryIgnoredException,
|
||||
# ldap errors
|
||||
@ -101,8 +96,6 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
return None
|
||||
if "logger" in event:
|
||||
if event["logger"] in [
|
||||
"dbbackup",
|
||||
"botocore",
|
||||
"kombu",
|
||||
"asyncio",
|
||||
"multiprocessing",
|
||||
|
@ -55,6 +55,10 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
|
||||
first_msg = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logger = get_logger()
|
||||
|
||||
def connect(self):
|
||||
super().connect()
|
||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||
@ -65,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
)
|
||||
if not outpost:
|
||||
raise DenyConnection()
|
||||
self.logger = get_logger().bind(outpost=outpost)
|
||||
self.logger = self.logger.bind(outpost=outpost)
|
||||
try:
|
||||
self.accept()
|
||||
except RuntimeError as exc:
|
||||
|
@ -2,6 +2,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
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 authentik.outposts.controllers.k8s.triggers import NeedsRecreate
|
||||
@ -16,10 +17,31 @@ def get_namespace() -> str:
|
||||
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"""
|
||||
if len(current) != len(reference):
|
||||
raise NeedsRecreate()
|
||||
for port in reference:
|
||||
if port not in current:
|
||||
if not any(compare_port(port, current_port) for current_port in current):
|
||||
raise NeedsRecreate()
|
||||
|
@ -3,6 +3,8 @@ import os
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from docker.errors import DockerException
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
HEADER = "### Managed by authentik"
|
||||
@ -27,6 +29,8 @@ class DockerInlineSSH:
|
||||
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
|
||||
self.host = host
|
||||
self.keypair = keypair
|
||||
if not self.keypair:
|
||||
raise DockerException("keypair must be set for SSH connections")
|
||||
self.config_path = Path("~/.ssh/config").expanduser()
|
||||
self.header = f"{HEADER} - {self.host}\n"
|
||||
|
||||
|
@ -23,6 +23,7 @@ from authentik.events.monitored_tasks import (
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.reflection import path_to_class
|
||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||
from authentik.outposts.controllers.docker import DockerClient
|
||||
@ -231,6 +232,9 @@ def _outpost_single_update(outpost: Outpost, layer=None):
|
||||
@CELERY_APP.task()
|
||||
def outpost_local_connection():
|
||||
"""Checks the local environment and create Service connections."""
|
||||
if not CONFIG.y_bool("outposts.discover"):
|
||||
LOGGER.debug("outpost integration discovery is disabled")
|
||||
return
|
||||
# Explicitly check against token filename, as that's
|
||||
# only present when the integration is enabled
|
||||
if Path(SERVICE_TOKEN_FILENAME).exists():
|
||||
|
@ -45,6 +45,13 @@ class GrantTypes(models.TextChoices):
|
||||
HYBRID = "hybrid"
|
||||
|
||||
|
||||
class ResponseMode(models.TextChoices):
|
||||
"""https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#OAuth.Post"""
|
||||
|
||||
QUERY = "query"
|
||||
FRAGMENT = "fragment"
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generateed, for compatibility reasons"""
|
||||
|
||||
|
@ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris="http://local.invalid/Foo",
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
@ -51,7 +51,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
"request": "foo",
|
||||
},
|
||||
)
|
||||
@ -105,26 +105,30 @@ class TestAuthorize(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris="http://local.invalid/Foo",
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type,
|
||||
GrantTypes.AUTHORIZATION_CODE,
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).redirect_uri,
|
||||
"http://local.invalid/Foo",
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
"scope": "openid",
|
||||
"state": "foo",
|
||||
},
|
||||
@ -140,7 +144,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
"state": "foo",
|
||||
},
|
||||
)
|
||||
@ -153,7 +157,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
data={
|
||||
"response_type": "code token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
"scope": "openid",
|
||||
"state": "foo",
|
||||
},
|
||||
@ -167,7 +171,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
data={
|
||||
"response_type": "invalid",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://local.invalid/Foo",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
@ -44,6 +44,7 @@ from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
GrantTypes,
|
||||
OAuth2Provider,
|
||||
ResponseMode,
|
||||
ResponseTypes,
|
||||
)
|
||||
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
|
||||
@ -99,7 +100,7 @@ class OAuthAuthorizationParams:
|
||||
# and POST request.
|
||||
query_dict = request.POST if request.method == "POST" else request.GET
|
||||
state = query_dict.get("state")
|
||||
redirect_uri = query_dict.get("redirect_uri", "").lower()
|
||||
redirect_uri = query_dict.get("redirect_uri", "")
|
||||
|
||||
response_type = query_dict.get("response_type", "")
|
||||
grant_type = None
|
||||
@ -153,7 +154,10 @@ class OAuthAuthorizationParams:
|
||||
def check_redirect_uri(self):
|
||||
"""Redirect URI validation."""
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
if not self.redirect_uri:
|
||||
# We don't want to actually lowercase the final URL we redirect to,
|
||||
# we only lowercase it for comparison
|
||||
redirect_uri = self.redirect_uri.lower()
|
||||
if not redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError("", allowed_redirect_urls)
|
||||
|
||||
@ -169,7 +173,7 @@ class OAuthAuthorizationParams:
|
||||
allow=self.redirect_uri,
|
||||
)
|
||||
return
|
||||
if self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
|
||||
if redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
@ -299,13 +303,23 @@ class OAuthFulfillmentStage(StageView):
|
||||
code = self.params.create_code(self.request)
|
||||
code.save(force_insert=True)
|
||||
|
||||
if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
|
||||
query_dict = self.request.POST if self.request.method == "POST" else self.request.GET
|
||||
response_mode = ResponseMode.QUERY
|
||||
# Get response mode from url param, otherwise decide based on grant type
|
||||
if "response_mode" in query_dict:
|
||||
response_mode = query_dict["response_mode"]
|
||||
elif self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
|
||||
response_mode = ResponseMode.QUERY
|
||||
elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
|
||||
response_mode = ResponseMode.FRAGMENT
|
||||
|
||||
if response_mode == ResponseMode.QUERY:
|
||||
query_params["code"] = code.code
|
||||
query_params["state"] = [str(self.params.state) if self.params.state else ""]
|
||||
|
||||
uri = uri._replace(query=urlencode(query_params, doseq=True))
|
||||
return urlunsplit(uri)
|
||||
if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
|
||||
if response_mode == ResponseMode.FRAGMENT:
|
||||
query_fragment = self.create_implicit_response(code)
|
||||
|
||||
uri = uri._replace(
|
||||
|
@ -12,4 +12,8 @@ class AuthentikProviderProxyConfig(AppConfig):
|
||||
verbose_name = "authentik Providers.Proxy"
|
||||
|
||||
def ready(self) -> None:
|
||||
from authentik.providers.proxy.tasks import proxy_set_defaults
|
||||
|
||||
import_module("authentik.providers.proxy.managed")
|
||||
|
||||
proxy_set_defaults.delay()
|
||||
|
@ -28,12 +28,12 @@ class ProxyDockerController(DockerController):
|
||||
labels["traefik.enable"] = "true"
|
||||
labels[
|
||||
f"traefik.http.routers.{traefik_name}-router.rule"
|
||||
] = f"Host({','.join(hosts)}) && PathPrefix(`/akprox`)"
|
||||
] = f"Host({','.join(hosts)}) && PathPrefix(`/outpost.goauthentik.io`)"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"
|
||||
labels[
|
||||
f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"
|
||||
] = "/akprox/ping"
|
||||
] = "/outpost.goauthentik.io/ping"
|
||||
labels[
|
||||
f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.port"
|
||||
] = "9300"
|
||||
|
@ -92,6 +92,8 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
# Buffer sizes for large headers with JWTs
|
||||
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
|
||||
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
|
||||
# Enable TLS in traefik
|
||||
"traefik.ingress.kubernetes.io/router.tls": "true",
|
||||
}
|
||||
annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations)
|
||||
return annotations
|
||||
@ -126,7 +128,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
port=V1ServiceBackendPort(name="http"),
|
||||
),
|
||||
),
|
||||
path="/akprox",
|
||||
path="/outpost.goauthentik.io",
|
||||
path_type="ImplementationSpecific",
|
||||
)
|
||||
]
|
||||
|
@ -119,7 +119,10 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||
),
|
||||
spec=TraefikMiddlewareSpec(
|
||||
forwardAuth=TraefikMiddlewareSpecForwardAuth(
|
||||
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
|
||||
address=(
|
||||
f"http://{self.name}.{self.namespace}:9000/"
|
||||
"outpost.goauthentik.io/auth/traefik"
|
||||
),
|
||||
authResponseHeaders=[
|
||||
"X-authentik-username",
|
||||
"X-authentik-groups",
|
||||
|
@ -27,7 +27,7 @@ def get_cookie_secret():
|
||||
|
||||
|
||||
def _get_callback_url(uri: str) -> str:
|
||||
return urljoin(uri, "/akprox/callback")
|
||||
return urljoin(uri, "outpost.goauthentik.io/callback")
|
||||
|
||||
|
||||
class ProxyMode(models.TextChoices):
|
||||
|
11
authentik/providers/proxy/tasks.py
Normal file
11
authentik/providers/proxy/tasks.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""proxy provider tasks"""
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def proxy_set_defaults():
|
||||
"""Ensure correct defaults are set for all providers"""
|
||||
for provider in ProxyProvider.objects.all():
|
||||
provider.set_oauth_defaults()
|
||||
provider.save()
|
@ -15,6 +15,7 @@ from authentik.providers.saml.processors.request_parser import AuthNRequestParse
|
||||
from authentik.sources.saml.exceptions import MismatchedRequestID
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
SAML_BINDING_REDIRECT,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
)
|
||||
@ -98,6 +99,9 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
# First create an AuthNRequest
|
||||
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()
|
||||
# Now we check the ID and signature
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
|
@ -6,7 +6,6 @@ import os
|
||||
import sys
|
||||
from hashlib import sha512
|
||||
from json import dumps
|
||||
from tempfile import gettempdir
|
||||
from time import time
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
@ -137,7 +136,6 @@ INSTALLED_APPS = [
|
||||
"guardian",
|
||||
"django_prometheus",
|
||||
"channels",
|
||||
"dbbackup",
|
||||
]
|
||||
|
||||
GUARDIAN_MONKEY_PATCH = False
|
||||
@ -357,32 +355,6 @@ CELERY_RESULT_BACKEND = (
|
||||
f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}"
|
||||
)
|
||||
|
||||
# Database backup
|
||||
DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage"
|
||||
DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"}
|
||||
DBBACKUP_FILENAME_TEMPLATE = f"authentik-backup-{__version__}-{{datetime}}.sql"
|
||||
DBBACKUP_CONNECTOR_MAPPING = {
|
||||
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
||||
}
|
||||
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
|
||||
DBBACKUP_CLEANUP_KEEP = 10
|
||||
if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
|
||||
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
DBBACKUP_STORAGE_OPTIONS = {
|
||||
"access_key": CONFIG.y("postgresql.s3_backup.access_key"),
|
||||
"secret_key": CONFIG.y("postgresql.s3_backup.secret_key"),
|
||||
"bucket_name": CONFIG.y("postgresql.s3_backup.bucket"),
|
||||
"region_name": CONFIG.y("postgresql.s3_backup.region", "eu-central-1"),
|
||||
"default_acl": "private",
|
||||
"endpoint_url": CONFIG.y("postgresql.s3_backup.host"),
|
||||
"location": CONFIG.y("postgresql.s3_backup.location", ""),
|
||||
"verify": not CONFIG.y_bool("postgresql.s3_backup.insecure_skip_verify", False),
|
||||
}
|
||||
j_print(
|
||||
"Database backup to S3 is configured",
|
||||
host=CONFIG.y("postgresql.s3_backup.host"),
|
||||
)
|
||||
|
||||
# Sentry integration
|
||||
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||
|
||||
@ -493,12 +465,9 @@ _LOGGING_HANDLER_MAP = {
|
||||
"urllib3": "WARNING",
|
||||
"websockets": "WARNING",
|
||||
"daphne": "WARNING",
|
||||
"dbbackup": "ERROR",
|
||||
"kubernetes": "INFO",
|
||||
"asyncio": "WARNING",
|
||||
"aioredis": "WARNING",
|
||||
"s3transfer": "WARNING",
|
||||
"botocore": "WARNING",
|
||||
}
|
||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
@ -1,13 +1,13 @@
|
||||
"""Sync LDAP Users and groups into authentik"""
|
||||
from typing import Any
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.db.models.base import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
|
||||
@ -123,8 +123,8 @@ class BaseLDAPSynchronizer:
|
||||
continue
|
||||
setattr(instance, key, value)
|
||||
final_atttributes = {}
|
||||
always_merger.merge(final_atttributes, instance.attributes)
|
||||
always_merger.merge(final_atttributes, data.get("attributes", {}))
|
||||
MERGE_LIST_UNIQUE.merge(final_atttributes, instance.attributes)
|
||||
MERGE_LIST_UNIQUE.merge(final_atttributes, data.get("attributes", {}))
|
||||
instance.attributes = final_atttributes
|
||||
instance.save()
|
||||
return (instance, False)
|
||||
|
@ -3,6 +3,7 @@ from ldap3.core.exceptions import LDAPException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
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.root.celery import CELERY_APP
|
||||
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:
|
||||
# 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))
|
||||
|
@ -18,6 +18,8 @@ from authentik.sources.saml.processors.constants import (
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
RSA_SHA512,
|
||||
SAML_BINDING_POST,
|
||||
SAML_BINDING_REDIRECT,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
@ -37,6 +39,15 @@ class SAMLBindingTypes(models.TextChoices):
|
||||
POST = "POST", _("POST Binding")
|
||||
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):
|
||||
"""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.encoding import deflate_and_base64_encode
|
||||
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 (
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
@ -62,7 +62,7 @@ class RequestProcessor:
|
||||
auth_n_request.attrib["Destination"] = self.source.sso_url
|
||||
auth_n_request.attrib["ID"] = self.request_id
|
||||
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"
|
||||
# Create issuer object
|
||||
auth_n_request.append(self.get_issuer())
|
||||
|
@ -13,8 +13,8 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||
|
||||
def validate_not_configured_action(self, value):
|
||||
"""Ensure that a configuration stage is set when not_configured_action is configure"""
|
||||
configuration_stage = self.initial_data.get("configuration_stage")
|
||||
if value == NotConfiguredAction.CONFIGURE and configuration_stage is None:
|
||||
configuration_stages = self.initial_data.get("configuration_stages")
|
||||
if value == NotConfiguredAction.CONFIGURE and configuration_stages is None:
|
||||
raise ValidationError(
|
||||
(
|
||||
'When "Not configured action" is set to "Configure", '
|
||||
@ -29,7 +29,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"not_configured_action",
|
||||
"device_classes",
|
||||
"configuration_stage",
|
||||
"configuration_stages",
|
||||
]
|
||||
|
||||
|
||||
@ -38,5 +38,5 @@ class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
queryset = AuthenticatorValidateStage.objects.all()
|
||||
serializer_class = AuthenticatorValidateStageSerializer
|
||||
filterset_fields = ["name", "not_configured_action", "configuration_stage"]
|
||||
filterset_fields = ["name", "not_configured_action", "configuration_stages"]
|
||||
ordering = ["name"]
|
||||
|
@ -0,0 +1,44 @@
|
||||
# Generated by Django 4.0.1 on 2022-01-05 22:09
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
AuthenticatorValidateStage = apps.get_model(
|
||||
"authentik_stages_authenticator_validate", "AuthenticatorValidateStage"
|
||||
)
|
||||
|
||||
for stage in AuthenticatorValidateStage.objects.using(db_alias).all():
|
||||
if stage.configuration_stage:
|
||||
stage.configuration_stages.set([stage.configuration_stage])
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0021_auto_20211227_2103"),
|
||||
("authentik_stages_authenticator_validate", "0009_default_stage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="configuration_stages",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.",
|
||||
related_name="+",
|
||||
to="authentik_flows.Stage",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_configuration_stage),
|
||||
migrations.RemoveField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="configuration_stage",
|
||||
),
|
||||
]
|
@ -38,16 +38,14 @@ class AuthenticatorValidateStage(Stage):
|
||||
choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
|
||||
)
|
||||
|
||||
configuration_stage = models.ForeignKey(
|
||||
configuration_stages = models.ManyToManyField(
|
||||
Stage,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
related_name="+",
|
||||
help_text=_(
|
||||
(
|
||||
"Stage used to configure Authenticator when user doesn't have any compatible "
|
||||
"Stages used to configure Authenticator when user doesn't have any compatible "
|
||||
"devices. After this configuration Stage passes, the user is not prompted again."
|
||||
)
|
||||
),
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Authenticator Validation"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django_otp import devices_for_user
|
||||
from rest_framework.fields import CharField, IntegerField, JSONField, ListField
|
||||
from rest_framework.fields import CharField, IntegerField, JSONField, ListField, UUIDField
|
||||
from rest_framework.serializers import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
||||
@ -26,6 +28,18 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
|
||||
SESSION_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
|
||||
SESSION_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
|
||||
|
||||
|
||||
class SelectableStageSerializer(PassiveSerializer):
|
||||
"""Serializer for stages which can be selected by users"""
|
||||
|
||||
pk = UUIDField()
|
||||
name = CharField()
|
||||
verbose_name = CharField()
|
||||
meta_model_name = CharField()
|
||||
|
||||
|
||||
class AuthenticatorValidationChallenge(WithUserInfoChallenge):
|
||||
@ -33,12 +47,14 @@ class AuthenticatorValidationChallenge(WithUserInfoChallenge):
|
||||
|
||||
device_challenges = ListField(child=DeviceChallenge())
|
||||
component = CharField(default="ak-stage-authenticator-validate")
|
||||
configuration_stages = ListField(child=SelectableStageSerializer())
|
||||
|
||||
|
||||
class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
"""Challenge used for Code-based and WebAuthn authenticators"""
|
||||
|
||||
selected_challenge = DeviceChallenge(required=False)
|
||||
selected_stage = CharField(required=False)
|
||||
|
||||
code = CharField(required=False)
|
||||
webauthn = JSONField(required=False)
|
||||
@ -46,7 +62,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
component = CharField(default="ak-stage-authenticator-validate")
|
||||
|
||||
def _challenge_allowed(self, classes: list):
|
||||
device_challenges: list[dict] = self.stage.request.session.get("device_challenges")
|
||||
device_challenges: list[dict] = self.stage.request.session.get(SESSION_DEVICE_CHALLENGES)
|
||||
if not any(x["device_class"] in classes for x in device_challenges):
|
||||
raise ValidationError("No compatible device class allowed")
|
||||
|
||||
@ -71,7 +87,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
def validate_selected_challenge(self, challenge: dict) -> dict:
|
||||
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
||||
# First check if the challenge is valid
|
||||
for device_challenge in self.stage.request.session.get("device_challenges"):
|
||||
for device_challenge in self.stage.request.session.get(SESSION_DEVICE_CHALLENGES):
|
||||
if device_challenge.get("device_class", "") != challenge.get("device_class", ""):
|
||||
raise ValidationError("invalid challenge selected")
|
||||
if device_challenge.get("device_uid", "") != challenge.get("device_uid", ""):
|
||||
@ -84,6 +100,15 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
select_challenge(self.stage.request, devices.first())
|
||||
return challenge
|
||||
|
||||
def validate_selected_stage(self, stage_pk: str) -> str:
|
||||
"""Check that the selected stage is valid"""
|
||||
stages = self.stage.request.session.get(SESSION_STAGES, [])
|
||||
if not any(str(stage.pk) == stage_pk for stage in stages):
|
||||
raise ValidationError("Selected stage is invalid")
|
||||
LOGGER.debug("Setting selected stage to ", stage=stage_pk)
|
||||
self.stage.request.session[SESSION_SELECTED_STAGE] = stage_pk
|
||||
return stage_pk
|
||||
|
||||
def validate(self, attrs: dict):
|
||||
# Checking if the given data is from a valid device class is done above
|
||||
# Here we only check if the any data was sent at all
|
||||
@ -164,7 +189,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
else:
|
||||
LOGGER.debug("No pending user, continuing")
|
||||
return self.executor.stage_ok()
|
||||
self.request.session["device_challenges"] = challenges
|
||||
self.request.session[SESSION_DEVICE_CHALLENGES] = challenges
|
||||
|
||||
# No allowed devices
|
||||
if len(challenges) < 1:
|
||||
@ -175,32 +200,74 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
LOGGER.debug("Authenticator not configured, denying")
|
||||
return self.executor.stage_invalid()
|
||||
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
|
||||
if not stage.configuration_stage:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
"Authenticator validation stage is set to configure user "
|
||||
"but no configuration flow is set."
|
||||
),
|
||||
stage=self,
|
||||
).from_http(self.request).set_user(user).save()
|
||||
return self.executor.stage_invalid()
|
||||
LOGGER.debug("Authenticator not configured, sending user to configure")
|
||||
# Because the foreign key to stage.configuration_stage points to
|
||||
# a base stage class, we need to do another lookup
|
||||
stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk)
|
||||
# plan.insert inserts at 1 index, so when stage_ok pops 0,
|
||||
# the configuration stage is next
|
||||
self.executor.plan.insert_stage(stage)
|
||||
return self.executor.stage_ok()
|
||||
LOGGER.debug("Authenticator not configured, forcing configure")
|
||||
return self.prepare_stages(user)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def prepare_stages(self, user: User, *args, **kwargs) -> HttpResponse:
|
||||
"""Check how the user can configure themselves. If no stages are set, return an error.
|
||||
If a single stage is set, insert that stage directly. If multiple are selected, include
|
||||
them in the challenge."""
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
if not stage.configuration_stages.exists():
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
"Authenticator validation stage is set to configure user "
|
||||
"but no configuration flow is set."
|
||||
),
|
||||
stage=self,
|
||||
).from_http(self.request).set_user(user).save()
|
||||
return self.executor.stage_invalid()
|
||||
if stage.configuration_stages.count() == 1:
|
||||
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
||||
LOGGER.debug("Single stage configured, auto-selecting", stage=next_stage)
|
||||
self.request.session[SESSION_SELECTED_STAGE] = next_stage
|
||||
# Because that normal insetion only happens on post, we directly inject it here and
|
||||
# return it
|
||||
self.executor.plan.insert_stage(next_stage)
|
||||
return self.executor.stage_ok()
|
||||
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
||||
self.request.session[SESSION_STAGES] = stages
|
||||
return super().get(self.request, *args, **kwargs)
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
res = super().post(request, *args, **kwargs)
|
||||
if (
|
||||
SESSION_SELECTED_STAGE in self.request.session
|
||||
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
||||
):
|
||||
LOGGER.debug("Got selected stage in session, running that")
|
||||
stage_pk = self.request.session.get(SESSION_SELECTED_STAGE)
|
||||
# Because the foreign key to stage.configuration_stage points to
|
||||
# a base stage class, we need to do another lookup
|
||||
stage = Stage.objects.get_subclass(pk=stage_pk)
|
||||
# plan.insert inserts at 1 index, so when stage_ok pops 0,
|
||||
# the configuration stage is next
|
||||
self.executor.plan.insert_stage(stage)
|
||||
return self.executor.stage_ok()
|
||||
return res
|
||||
|
||||
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
||||
challenges = self.request.session["device_challenges"]
|
||||
challenges = self.request.session.get(SESSION_DEVICE_CHALLENGES, [])
|
||||
stages = self.request.session.get(SESSION_STAGES, [])
|
||||
stage_challenges = []
|
||||
for stage in stages:
|
||||
serializer = SelectableStageSerializer(
|
||||
data={
|
||||
"pk": stage.pk,
|
||||
"name": stage.name,
|
||||
"verbose_name": str(stage._meta.verbose_name),
|
||||
"meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}",
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
stage_challenges.append(serializer.data)
|
||||
return AuthenticatorValidationChallenge(
|
||||
data={
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"device_challenges": challenges,
|
||||
"configuration_stages": stage_challenges,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -43,8 +43,8 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name="foo",
|
||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||
configuration_stage=conf_stage,
|
||||
)
|
||||
stage.configuration_stages.set([conf_stage])
|
||||
flow = Flow.objects.create(name="test", slug="test", title="test")
|
||||
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -38,7 +38,7 @@ services:
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
8
go.mod
8
go.mod
@ -8,16 +8,16 @@ require (
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/garyburd/redigo v1.6.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.12.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-openapi/runtime v0.22.0
|
||||
github.com/go-openapi/strfmt v0.21.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.2
|
||||
github.com/go-openapi/runtime v0.23.0
|
||||
github.com/go-openapi/strfmt v0.21.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
|
16
go.sum
16
go.sum
@ -125,8 +125,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8=
|
||||
github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
@ -183,8 +183,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g
|
||||
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
|
||||
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
|
||||
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
|
||||
github.com/go-openapi/runtime v0.22.0 h1:vY2D0u807kkcwidaj0YJuq4zyAWQnjLNDpJcVBrUFNs=
|
||||
github.com/go-openapi/runtime v0.22.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
|
||||
github.com/go-openapi/runtime v0.23.0 h1:HX6ET2sHCIvaKeDDQoU01CtO1ekg5EkekHSkLTtWXH0=
|
||||
github.com/go-openapi/runtime v0.23.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||
@ -208,8 +208,8 @@ github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLs
|
||||
github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
||||
github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
|
||||
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
|
||||
github.com/go-openapi/strfmt v0.21.1 h1:G6s2t5V5kGCHLVbSdZ/6lI8Wm4OzoPFkc3/cjAsKQrM=
|
||||
github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k=
|
||||
github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os=
|
||||
github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
@ -334,8 +334,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
@ -25,4 +25,4 @@ func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2022.1.4"
|
||||
const VERSION = "2022.2.1"
|
||||
|
@ -25,7 +25,7 @@ var (
|
||||
func RunServer() {
|
||||
m := mux.NewRouter()
|
||||
l := log.WithField("logger", "authentik.outpost.metrics")
|
||||
m.HandleFunc("/akprox/ping", func(rw http.ResponseWriter, r *http.Request) {
|
||||
m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(204)
|
||||
})
|
||||
m.Path("/metrics").Handler(promhttp.Handler())
|
||||
|
@ -78,7 +78,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: *p.ClientId,
|
||||
ClientSecret: *p.ClientSecret,
|
||||
RedirectURL: urlJoin(p.ExternalHost, "/akprox/callback"),
|
||||
RedirectURL: urlJoin(p.ExternalHost, "/outpost.goauthentik.io/callback"),
|
||||
Endpoint: endpoint.Endpoint,
|
||||
Scopes: p.ScopesToRequest,
|
||||
}
|
||||
@ -145,10 +145,10 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
|
||||
|
||||
// Support /start and /sign_in for backwards compatibility
|
||||
mux.HandleFunc("/akprox/start", a.handleRedirect)
|
||||
mux.HandleFunc("/akprox/sign_in", a.handleRedirect)
|
||||
mux.HandleFunc("/akprox/callback", a.handleCallback)
|
||||
mux.HandleFunc("/akprox/sign_out", a.handleSignOut)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/start", a.handleRedirect)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/sign_in", a.handleRedirect)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleCallback)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
|
||||
switch *p.Mode {
|
||||
case api.PROXYMODE_PROXY:
|
||||
err = a.configureProxy()
|
||||
|
@ -6,14 +6,14 @@ type ProxyClaims struct {
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Exp int `json:"exp"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Proxy ProxyClaims `json:"ak_proxy"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
Sub string `json:"sub"`
|
||||
Exp int `json:"exp"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Proxy *ProxyClaims `json:"ak_proxy"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
|
||||
RawToken string
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ func (a *Application) ErrorPage(rw http.ResponseWriter, r *http.Request, err str
|
||||
data := ErrorPageData{
|
||||
Title: "Bad Gateway",
|
||||
Message: "Error proxying to upstream server",
|
||||
ProxyPrefix: "/akprox",
|
||||
ProxyPrefix: "/outpost.goauthentik.io",
|
||||
}
|
||||
if claims != nil && len(err) > 0 {
|
||||
data.Message = err
|
||||
|
@ -12,15 +12,15 @@ import (
|
||||
)
|
||||
|
||||
func (a *Application) configureForward() error {
|
||||
a.mux.HandleFunc("/akprox/auth", func(rw http.ResponseWriter, r *http.Request) {
|
||||
a.mux.HandleFunc("/outpost.goauthentik.io/auth", func(rw http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := r.URL.Query()["traefik"]; ok {
|
||||
a.forwardHandleTraefik(rw, r)
|
||||
return
|
||||
}
|
||||
a.forwardHandleNginx(rw, r)
|
||||
})
|
||||
a.mux.HandleFunc("/akprox/auth/traefik", a.forwardHandleTraefik)
|
||||
a.mux.HandleFunc("/akprox/auth/nginx", a.forwardHandleNginx)
|
||||
a.mux.HandleFunc("/outpost.goauthentik.io/auth/traefik", a.forwardHandleTraefik)
|
||||
a.mux.HandleFunc("/outpost.goauthentik.io/auth/nginx", a.forwardHandleNginx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -49,8 +49,8 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/akprox") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
|
||||
if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/outpost.goauthentik.io") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access")
|
||||
return
|
||||
}
|
||||
host := ""
|
||||
@ -80,7 +80,7 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
|
||||
if proto != "" {
|
||||
proto = proto + ":"
|
||||
}
|
||||
rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/akprox/start")
|
||||
rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/outpost.goauthentik.io/start")
|
||||
a.log.WithField("url", rdFinal).Debug("Redirecting to login")
|
||||
http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect)
|
||||
}
|
||||
@ -119,8 +119,8 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
if fwd.String() != r.URL.String() {
|
||||
if strings.HasPrefix(fwd.Path, "/akprox") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
|
||||
if strings.HasPrefix(fwd.Path, "/outpost.goauthentik.io") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestForwardHandleNginx_Single_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
@ -22,7 +22,7 @@ func TestForwardHandleNginx_Single_Blank(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Single_Skip(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
@ -33,7 +33,7 @@ func TestForwardHandleNginx_Single_Skip(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Single_Headers(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
@ -47,7 +47,7 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Single_URI(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "https://foo.bar/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
@ -61,7 +61,7 @@ func TestForwardHandleNginx_Single_URI(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
@ -70,7 +70,7 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
s.Values[constants.SessionClaims] = Claims{
|
||||
Sub: "foo",
|
||||
Proxy: ProxyClaims{
|
||||
Proxy: &ProxyClaims{
|
||||
UserAttributes: map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
@ -108,7 +108,7 @@ func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
@ -121,7 +121,7 @@ func TestForwardHandleNginx_Domain_Header(t *testing.T) {
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestForwardHandleTraefik_Single_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
@ -22,7 +22,7 @@ func TestForwardHandleTraefik_Single_Blank(t *testing.T) {
|
||||
|
||||
func TestForwardHandleTraefik_Single_Skip(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/skip")
|
||||
@ -35,7 +35,7 @@ func TestForwardHandleTraefik_Single_Skip(t *testing.T) {
|
||||
|
||||
func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
@ -45,7 +45,7 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
|
||||
|
||||
assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, loc.String(), "http://test.goauthentik.io/akprox/start")
|
||||
assert.Equal(t, loc.String(), "http://test.goauthentik.io/outpost.goauthentik.io/start")
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
@ -53,7 +53,7 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
|
||||
|
||||
func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
@ -64,7 +64,7 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
s.Values[constants.SessionClaims] = Claims{
|
||||
Sub: "foo",
|
||||
Proxy: ProxyClaims{
|
||||
Proxy: &ProxyClaims{
|
||||
UserAttributes: map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
@ -102,7 +102,7 @@ func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
@ -115,7 +115,7 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
@ -125,7 +125,7 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "http://auth.test.goauthentik.io/akprox/start", loc.String())
|
||||
assert.Equal(t, "http://auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
|
@ -73,18 +73,20 @@ func (a *Application) configureProxy() error {
|
||||
|
||||
func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
|
||||
return func(r *http.Request) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
claims, _ := a.getClaims(r)
|
||||
if claims.Proxy.BackendOverride != "" {
|
||||
r.URL.Scheme = ou.Scheme
|
||||
r.URL.Host = ou.Host
|
||||
if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
|
||||
u, err := url.Parse(claims.Proxy.BackendOverride)
|
||||
if err != nil {
|
||||
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
||||
} else {
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
}
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
} else {
|
||||
r.URL.Scheme = ou.Scheme
|
||||
r.URL.Host = ou.Host
|
||||
}
|
||||
a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url")
|
||||
}
|
||||
}
|
||||
|
||||
|
82
internal/outpost/proxyv2/application/mode_proxy_test.go
Normal file
82
internal/outpost/proxyv2/application/mode_proxy_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
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, "frontend", req.Header.Get("X-Forwarded-Host"))
|
||||
assert.Equal(t, "/foo", req.URL.Path)
|
||||
assert.Equal(t, "backend:8012", req.URL.Host)
|
||||
assert.Equal(t, "frontend", 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, "frontend", 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, "frontend", req.Host)
|
||||
}
|
@ -3,14 +3,46 @@ package application
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
redirectParam = "rd"
|
||||
)
|
||||
|
||||
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
|
||||
rd := r.URL.Query().Get(redirectParam)
|
||||
if rd == "" {
|
||||
return "", false
|
||||
}
|
||||
u, err := url.Parse(rd)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("Failed to parse redirect URL")
|
||||
return "", false
|
||||
}
|
||||
// Check to make sure we only redirect to allowed places
|
||||
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
|
||||
if !strings.Contains(u.String(), a.proxyConfig.ExternalHost) {
|
||||
a.log.WithField("url", u.String()).WithField("ext", a.proxyConfig.ExternalHost).Warning("redirect URI did not contain external host")
|
||||
return "", false
|
||||
}
|
||||
} else {
|
||||
if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
|
||||
a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return u.String(), true
|
||||
}
|
||||
|
||||
func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
|
||||
newState := base64.RawStdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
|
||||
newState := base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
s.Values[constants.SessionOAuthState] = []string{}
|
||||
@ -20,6 +52,11 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
|
||||
s.Values[constants.SessionOAuthState] = []string{}
|
||||
state = []string{}
|
||||
}
|
||||
rd, ok := a.checkRedirectParam(r)
|
||||
if ok {
|
||||
s.Values[constants.SessionRedirect] = rd
|
||||
a.log.WithField("rd", rd).Trace("Setting redirect")
|
||||
}
|
||||
s.Values[constants.SessionOAuthState] = append(state, newState)
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
@ -29,7 +66,10 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) {
|
||||
s, _ := a.sessions.Get(r, constants.SeesionName)
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Trace("failed to get session")
|
||||
}
|
||||
state, ok := s.Values[constants.SessionOAuthState]
|
||||
if !ok {
|
||||
a.log.Warning("No state saved in session")
|
||||
@ -62,8 +102,8 @@ func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) {
|
||||
redirect := a.proxyConfig.ExternalHost
|
||||
redirectR, ok := s.Values[constants.SessionRedirect]
|
||||
if ok {
|
||||
a.log.WithField("redirect", redirectR).Trace("got final redirect from session")
|
||||
redirect = redirectR.(string)
|
||||
}
|
||||
a.log.WithField("redirect", redirect).Trace("final redirect")
|
||||
http.Redirect(rw, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
51
internal/outpost/proxyv2/application/oauth_test.go
Normal file
51
internal/outpost/proxyv2/application/oauth_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func TestCheckRedirectParam(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil)
|
||||
|
||||
rd, ok := a.checkRedirectParam(req)
|
||||
|
||||
assert.Equal(t, false, ok)
|
||||
assert.Equal(t, "", rd)
|
||||
|
||||
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
|
||||
|
||||
rd, ok = a.checkRedirectParam(req)
|
||||
|
||||
assert.Equal(t, false, ok)
|
||||
assert.Equal(t, "", rd)
|
||||
|
||||
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test", nil)
|
||||
|
||||
rd, ok = a.checkRedirectParam(req)
|
||||
|
||||
assert.Equal(t, true, ok)
|
||||
assert.Equal(t, "https://ext.t.goauthentik.io/test", rd)
|
||||
}
|
||||
|
||||
func TestCheckRedirectParam_Domain(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("t.goauthentik.io")
|
||||
req, _ := http.NewRequest("GET", "https://a.t.goauthentik.io/outpost.goauthentik.io/auth/start", nil)
|
||||
|
||||
rd, ok := a.checkRedirectParam(req)
|
||||
|
||||
assert.Equal(t, false, ok)
|
||||
assert.Equal(t, "", rd)
|
||||
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test", nil)
|
||||
|
||||
rd, ok = a.checkRedirectParam(req)
|
||||
|
||||
assert.Equal(t, true, ok)
|
||||
assert.Equal(t, "https://ext.t.goauthentik.io/test", rd)
|
||||
}
|
@ -15,6 +15,7 @@ func newTestApplication() *Application {
|
||||
ClientId: api.PtrString(ak.TestSecret()),
|
||||
ClientSecret: api.PtrString(ak.TestSecret()),
|
||||
CookieSecret: api.PtrString(ak.TestSecret()),
|
||||
ExternalHost: "https://ext.t.goauthentik.io",
|
||||
CookieDomain: api.PtrString(""),
|
||||
Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(),
|
||||
SkipPathRegex: api.PtrString("/skip.*"),
|
||||
|
@ -42,7 +42,7 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
|
||||
a.log.WithError(err).Warning("failed to save session before redirect")
|
||||
}
|
||||
|
||||
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start")
|
||||
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/outpost.goauthentik.io/start")
|
||||
http.Redirect(rw, r, authUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ func TestRedirectToStart_Proxy(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
|
||||
@ -38,7 +38,7 @@ func TestRedirectToStart_Forward(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
|
||||
@ -56,7 +56,7 @@ func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
|
||||
@ -74,7 +74,7 @@ func TestRedirectToStart_Forward_Domain(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
|
||||
|
@ -32,7 +32,7 @@ func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) {
|
||||
before := time.Now()
|
||||
web.DisableIndex(http.StripPrefix("/akprox/static/dist", staticWeb.StaticHandler)).ServeHTTP(rw, r)
|
||||
web.DisableIndex(http.StripPrefix("/outpost.goauthentik.io/static/dist", staticWeb.StaticHandler)).ServeHTTP(rw, r)
|
||||
after := time.Since(before)
|
||||
metrics.Requests.With(prometheus.Labels{
|
||||
"outpost_name": ps.akAPI.Outpost.Name,
|
||||
@ -90,11 +90,11 @@ func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, str
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/akprox/static") {
|
||||
if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/static") {
|
||||
ps.HandleStatic(rw, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/akprox/ping") {
|
||||
if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/ping") {
|
||||
ps.HandlePing(rw, r)
|
||||
return
|
||||
}
|
||||
@ -109,6 +109,7 @@ func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
ps.log.WithField("headers", r.Header).Trace("tracing headers for no hostname match")
|
||||
ps.log.WithField("host", host).Warning("no app for hostname")
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
@ -25,7 +25,7 @@ var (
|
||||
func RunServer() {
|
||||
m := mux.NewRouter()
|
||||
l := log.WithField("logger", "authentik.outpost.metrics")
|
||||
m.HandleFunc("/akprox/ping", func(rw http.ResponseWriter, r *http.Request) {
|
||||
m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(204)
|
||||
})
|
||||
m.Path("/metrics").Handler(promhttp.Handler())
|
||||
|
@ -64,8 +64,8 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
|
||||
akAPI: ac,
|
||||
defaultCert: defaultCert,
|
||||
}
|
||||
globalMux.PathPrefix("/akprox/static").HandlerFunc(s.HandleStatic)
|
||||
globalMux.Path("/akprox/ping").HandlerFunc(s.HandlePing)
|
||||
globalMux.PathPrefix("/outpost.goauthentik.io/static").HandlerFunc(s.HandleStatic)
|
||||
globalMux.Path("/outpost.goauthentik.io/ping").HandlerFunc(s.HandlePing)
|
||||
rootMux.PathPrefix("/").HandlerFunc(s.Handle)
|
||||
return s
|
||||
}
|
||||
@ -102,7 +102,11 @@ func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate {
|
||||
}
|
||||
|
||||
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 {
|
||||
return &ps.defaultCert, nil
|
||||
}
|
||||
|
@ -5,12 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/akprox/static/dist/assets/icons/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/akprox/static/dist/patternfly.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/akprox/static/dist/authentik.css">
|
||||
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
--ak-flow-background: url("/akprox/static/dist/assets/images/flow_background.jpg");
|
||||
--ak-flow-background: url("/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -32,7 +33,7 @@
|
||||
<div class="ak-login-container">
|
||||
<header class="pf-c-login__header">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="/akprox/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" />
|
||||
<img src="/outpost.goauthentik.io/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" />
|
||||
</div>
|
||||
</header>
|
||||
<main class="pf-c-login__main">
|
||||
|
@ -3,7 +3,8 @@ package templates
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//go:embed error.html
|
||||
|
@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@ -24,11 +25,12 @@ func (ws *WebServer) configureProxy() {
|
||||
if req.TLS != nil {
|
||||
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.ErrorHandler = ws.proxyErrorHandler
|
||||
rp.ModifyResponse = ws.proxyModifyResponse
|
||||
ws.m.PathPrefix("/akprox").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ws.m.PathPrefix("/outpost.goauthentik.io").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if ws.ProxyServer != nil {
|
||||
before := time.Now()
|
||||
ws.ProxyServer.Handle(rw, r)
|
||||
@ -65,9 +67,20 @@ func (ws *WebServer) configureProxy() {
|
||||
}
|
||||
|
||||
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)
|
||||
_, 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 {
|
||||
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 {
|
||||
r.Header.Set("X-Powered-By", "authentik")
|
||||
r.Header.Del("Server")
|
||||
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")
|
||||
}
|
||||
return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ch.ServerName == "" {
|
||||
return &cert, nil
|
||||
}
|
||||
if ws.ProxyServer != nil {
|
||||
appCert := ws.ProxyServer.GetCertificate(ch.ServerName)
|
||||
if appCert != nil {
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Stage 1: Build
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.7-bullseye AS builder
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -19,7 +19,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
COPY --from=builder /go/ldap /
|
||||
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ]
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ]
|
||||
|
||||
EXPOSE 3389 6636 9300
|
||||
|
||||
|
30
lifecycle/ak
30
lifecycle/ak
@ -32,30 +32,6 @@ function check_if_root {
|
||||
chpst -u authentik:$GROUP env HOME=/authentik $1
|
||||
}
|
||||
|
||||
function prefixwith {
|
||||
local prefix="$1"
|
||||
shift
|
||||
"$@" > >(sed "s/^/$prefix: /") 2> >(sed "s/^/$prefix (err): /" >&2)
|
||||
}
|
||||
|
||||
function restore {
|
||||
PG_HOST=$(python -m authentik.lib.config postgresql.host 2> /dev/null)
|
||||
PG_NAME=$(python -m authentik.lib.config postgresql.name 2> /dev/null)
|
||||
PG_USER=$(python -m authentik.lib.config postgresql.user 2> /dev/null)
|
||||
PG_PORT=$(python -m authentik.lib.config postgresql.port 2> /dev/null)
|
||||
export PGPASSWORD=$(python -m authentik.lib.config postgresql.password 2> /dev/null)
|
||||
log "Ensuring no one can connect to the database"
|
||||
prefixwith "psql" psql -h"${PG_HOST}" -U"${PG_USER}" -c"UPDATE pg_database SET datallowconn = 'false' WHERE datname = '${PG_NAME}';" "postgres"
|
||||
prefixwith "psql" psql -h"${PG_HOST}" -U"${PG_USER}" -c"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${PG_NAME}';" "postgres"
|
||||
log "deleting and re-creating database"
|
||||
prefixwith "psql" dropdb -h"${PG_HOST}" -U"${PG_USER}" "${PG_NAME}" || trueacku
|
||||
prefixwith "psql" createdb -h"${PG_HOST}" -U"${PG_USER}" "${PG_NAME}"
|
||||
log "running initial migrations"
|
||||
prefixwith "migrate" python -m lifecycle.migrate 2> /dev/null
|
||||
log "restoring database"
|
||||
prefixwith "restore" python -m manage dbrestore -i ${@:2}
|
||||
}
|
||||
|
||||
MODE_FILE="/tmp/authentik-mode"
|
||||
|
||||
if [[ "$1" == "server" ]]; then
|
||||
@ -75,12 +51,6 @@ elif [[ "$1" == "worker" ]]; then
|
||||
elif [[ "$1" == "flower" ]]; then
|
||||
echo "flower" > $MODE_FILE
|
||||
celery -A authentik.root.celery flower
|
||||
elif [[ "$1" == "backup" ]]; then
|
||||
wait_for_db
|
||||
python -m manage dbbackup --clean
|
||||
elif [[ "$1" == "restore" ]]; then
|
||||
wait_for_db
|
||||
restore $@
|
||||
elif [[ "$1" == "bash" ]]; then
|
||||
/bin/bash
|
||||
elif [[ "$1" == "test" ]]; then
|
||||
|
BIN
locale/zh-Hans/LC_MESSAGES/django.mo
Normal file
BIN
locale/zh-Hans/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1749
locale/zh-Hans/LC_MESSAGES/django.po
Normal file
1749
locale/zh-Hans/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
locale/zh-Hant/LC_MESSAGES/django.mo
Normal file
BIN
locale/zh-Hant/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1749
locale/zh-Hant/LC_MESSAGES/django.po
Normal file
1749
locale/zh-Hant/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
locale/zh_TW/LC_MESSAGES/django.mo
Normal file
BIN
locale/zh_TW/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1749
locale/zh_TW/LC_MESSAGES/django.po
Normal file
1749
locale/zh_TW/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,13 +22,6 @@ warnings.filterwarnings(
|
||||
"efault_app_config."
|
||||
),
|
||||
)
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=(
|
||||
"'dbbackup' defines default_app_config = 'dbbackup.apps.DbbackupConfig'. Django now det"
|
||||
"ects this configuration automatically. You can remove default_app_config."
|
||||
),
|
||||
)
|
||||
|
||||
defuse_stdlib()
|
||||
|
||||
|
631
poetry.lock
generated
631
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.7-bullseye AS builder
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -32,7 +32,7 @@ COPY --from=web-builder /static/security.txt /web/security.txt
|
||||
COPY --from=web-builder /static/dist/ /web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /web/authentik/
|
||||
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ]
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ]
|
||||
|
||||
EXPOSE 9000 9300 9443
|
||||
|
||||
|
@ -92,12 +92,11 @@ addopts = "-p no:celery --junitxml=unittest.xml"
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2022.1.4"
|
||||
version = "2022.2.1"
|
||||
description = ""
|
||||
authors = ["Jens Langhammer <jens.langhammer@beryju.org>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
boto3 = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
@ -107,14 +106,12 @@ dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-dbbackup = "=4.0.0b0"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-model-utils = "*"
|
||||
django-otp = "*"
|
||||
django-prometheus = "*"
|
||||
django-redis = "*"
|
||||
django-storages = "*"
|
||||
djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
|
72
schema.yml
72
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2022.1.4
|
||||
version: 2022.2.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -15045,10 +15045,14 @@ paths:
|
||||
description: AuthenticatorValidateStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: configuration_stage
|
||||
name: configuration_stages
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
@ -19826,11 +19830,12 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/DeviceClassesEnum'
|
||||
description: Device classes which can be used to authenticate
|
||||
configuration_stage:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Stage used to configure Authenticator when user doesn't have
|
||||
configuration_stages:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Stages used to configure Authenticator when user doesn't have
|
||||
any compatible devices. After this configuration Stage passes, the user
|
||||
is not prompted again.
|
||||
required:
|
||||
@ -19858,11 +19863,12 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/DeviceClassesEnum'
|
||||
description: Device classes which can be used to authenticate
|
||||
configuration_stage:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Stage used to configure Authenticator when user doesn't have
|
||||
configuration_stages:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Stages used to configure Authenticator when user doesn't have
|
||||
any compatible devices. After this configuration Stage passes, the user
|
||||
is not prompted again.
|
||||
required:
|
||||
@ -19892,7 +19898,12 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DeviceChallenge'
|
||||
configuration_stages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SelectableStage'
|
||||
required:
|
||||
- configuration_stages
|
||||
- device_challenges
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
@ -19907,6 +19918,9 @@ components:
|
||||
default: ak-stage-authenticator-validate
|
||||
selected_challenge:
|
||||
$ref: '#/components/schemas/DeviceChallengeRequest'
|
||||
selected_stage:
|
||||
type: string
|
||||
minLength: 1
|
||||
code:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -20017,7 +20031,6 @@ components:
|
||||
enum:
|
||||
- can_save_media
|
||||
- can_geo_ip
|
||||
- can_backup
|
||||
type: string
|
||||
CaptchaChallenge:
|
||||
type: object
|
||||
@ -26678,11 +26691,12 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/DeviceClassesEnum'
|
||||
description: Device classes which can be used to authenticate
|
||||
configuration_stage:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Stage used to configure Authenticator when user doesn't have
|
||||
configuration_stages:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Stages used to configure Authenticator when user doesn't have
|
||||
any compatible devices. After this configuration Stage passes, the user
|
||||
is not prompted again.
|
||||
PatchedCaptchaStageRequest:
|
||||
@ -30018,6 +30032,24 @@ components:
|
||||
- direct
|
||||
- cached
|
||||
type: string
|
||||
SelectableStage:
|
||||
type: object
|
||||
description: Serializer for stages which can be selected by users
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
verbose_name:
|
||||
type: string
|
||||
meta_model_name:
|
||||
type: string
|
||||
required:
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- verbose_name
|
||||
ServiceConnection:
|
||||
type: object
|
||||
description: ServiceConnection Serializer
|
||||
|
@ -105,7 +105,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text)
|
||||
self.assertIn("X-Foo: bar", full_body_text)
|
||||
|
||||
self.driver.get("http://localhost:9000/akprox/sign_out")
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of proxy.", full_body_text)
|
||||
|
873
web/package-lock.json
generated
873
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,11 @@
|
||||
"fr_FR",
|
||||
"tr",
|
||||
"es",
|
||||
"pl"
|
||||
"pl",
|
||||
"zh_TW",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"de"
|
||||
],
|
||||
"formatOptions": {
|
||||
"lineNumbers": false
|
||||
@ -48,14 +52,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "^7.16.10",
|
||||
"@babel/core": "^7.17.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.17.2",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@formatjs/intl-listformat": "^6.5.1",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@goauthentik/api": "^2022.1.3-1643236150",
|
||||
"@formatjs/intl-listformat": "^6.5.2",
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"@goauthentik/api": "^2022.1.5-1644681372",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.3.0",
|
||||
"@lingui/cli": "^3.13.2",
|
||||
"@lingui/core": "^3.13.2",
|
||||
@ -67,36 +71,36 @@
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"@rollup/plugin-replace": "^3.1.0",
|
||||
"@rollup/plugin-typescript": "^8.3.0",
|
||||
"@sentry/browser": "^6.17.3",
|
||||
"@sentry/tracing": "^6.17.3",
|
||||
"@sentry/browser": "^6.17.8",
|
||||
"@sentry/tracing": "^6.17.8",
|
||||
"@squoosh/cli": "^0.7.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.1.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
|
||||
"@types/chart.js": "^2.9.35",
|
||||
"@types/codemirror": "5.60.5",
|
||||
"@types/grecaptcha": "^3.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.6.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.7.0",
|
||||
"chart.js": "^3.7.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.65.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"country-flag-icons": "^1.4.20",
|
||||
"eslint": "^8.8.0",
|
||||
"country-flag-icons": "^1.4.21",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.4",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"flowchart.js": "^1.17.0",
|
||||
"flowchart.js": "^1.17.1",
|
||||
"fuse.js": "^6.5.3",
|
||||
"lit": "^2.1.2",
|
||||
"lit": "^2.1.4",
|
||||
"moment": "^2.29.1",
|
||||
"prettier": "^2.5.1",
|
||||
"rapidoc": "^9.1.4",
|
||||
"rollup": "^2.66.1",
|
||||
"rollup": "^2.67.2",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
"rollup-plugin-minify-html-literals": "^1.2.6",
|
||||
|
@ -34,6 +34,7 @@ export const resources = [
|
||||
dest: "dist/",
|
||||
},
|
||||
{ src: "src/authentik.css", dest: "dist/" },
|
||||
{ src: "src/custom.css", dest: "dist/" },
|
||||
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/assets/*",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 580 KiB |
@ -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 = "2022.1.4";
|
||||
export const VERSION = "2022.2.1";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
1
web/src/custom.css
Normal file
1
web/src/custom.css
Normal file
@ -0,0 +1 @@
|
||||
/* User customisable */
|
@ -433,8 +433,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
)
|
||||
? html`
|
||||
<li>
|
||||
<a
|
||||
href="https://unsplash.com/@kimonmaritz"
|
||||
<a href="https://unsplash.com/@trime"
|
||||
>${t`Background image`}</a
|
||||
>
|
||||
</li>
|
||||
|
@ -67,7 +67,7 @@ export class AuthenticatorValidateStage
|
||||
return this._selectedDeviceChallenge;
|
||||
}
|
||||
|
||||
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
|
||||
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> {
|
||||
return this.host?.submit(payload) || Promise.resolve();
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ export class AuthenticatorValidateStage
|
||||
}
|
||||
|
||||
renderDevicePicker(): TemplateResult {
|
||||
return html` <ul>
|
||||
return html`<ul>
|
||||
${this.challenge?.deviceChallenges.map((challenges) => {
|
||||
return html`<li>
|
||||
<button
|
||||
@ -157,6 +157,30 @@ export class AuthenticatorValidateStage
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
renderStagePicker(): TemplateResult {
|
||||
return html`<ul>
|
||||
${this.challenge?.configurationStages.map((stage) => {
|
||||
return html`<li>
|
||||
<button
|
||||
class="pf-c-button authenticator-button"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.submit({
|
||||
component: this.challenge.component || "",
|
||||
selectedStage: stage.pk,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div class="right">
|
||||
<p>${stage.name}</p>
|
||||
<small>${stage.verboseName}</small>
|
||||
</div>
|
||||
</button>
|
||||
</li>`;
|
||||
})}
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
renderDeviceChallenge(): TemplateResult {
|
||||
if (!this.selectedDeviceChallenge) {
|
||||
return html``;
|
||||
@ -242,6 +266,9 @@ export class AuthenticatorValidateStage
|
||||
${this.selectedDeviceChallenge
|
||||
? ""
|
||||
: html`<p>${t`Select an authentication method.`}</p>`}
|
||||
${this.challenge.configurationStages.length > 0
|
||||
? this.renderStagePicker()
|
||||
: html``}
|
||||
</form>
|
||||
${this.renderDevicePicker()}
|
||||
</div>
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { en, es, fr, pl, tr } from "make-plural/plurals";
|
||||
import { de, en, es, fr, pl, tr, zh } from "make-plural/plurals";
|
||||
|
||||
import { Messages, i18n } from "@lingui/core";
|
||||
import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale";
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { messages as localeDE } from "../locales/de";
|
||||
import { messages as localeEN } from "../locales/en";
|
||||
import { messages as localeES } from "../locales/es";
|
||||
import { messages as localeFR_FR } from "../locales/fr_FR";
|
||||
import { messages as localePL } from "../locales/pl";
|
||||
import { messages as localeDEBUG } from "../locales/pseudo-LOCALE";
|
||||
import { messages as localeTR } from "../locales/tr";
|
||||
import { messages as localeZH_Hans } from "../locales/zh-Hans";
|
||||
import { messages as localeZH_Hant } from "../locales/zh-Hant";
|
||||
import { messages as localeZH_TW } from "../locales/zh_TW";
|
||||
|
||||
export const LOCALES: {
|
||||
code: string;
|
||||
@ -54,6 +58,30 @@ export const LOCALES: {
|
||||
label: t`Polish`,
|
||||
locale: localePL,
|
||||
},
|
||||
{
|
||||
code: "zh_TW",
|
||||
plurals: zh,
|
||||
label: t`Taiwanese Mandarin`,
|
||||
locale: localeZH_TW,
|
||||
},
|
||||
{
|
||||
code: "zh-Hans",
|
||||
plurals: zh,
|
||||
label: t`Chinese (simplified)`,
|
||||
locale: localeZH_Hans,
|
||||
},
|
||||
{
|
||||
code: "zh-Hant",
|
||||
plurals: zh,
|
||||
label: t`Chinese (traditional)`,
|
||||
locale: localeZH_Hant,
|
||||
},
|
||||
{
|
||||
code: "de",
|
||||
plurals: de,
|
||||
label: t`German`,
|
||||
locale: localeDE,
|
||||
},
|
||||
];
|
||||
|
||||
LOCALES.forEach((locale) => {
|
||||
|
6125
web/src/locales/de.po
Normal file
6125
web/src/locales/de.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -563,16 +563,16 @@ msgid "Background shown during execution."
|
||||
msgstr "Background shown during execution."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr "Backup finished with errors."
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr "Backup finished with errors."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr "Backup finished with warnings/backup not supported."
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr "Backup finished with warnings/backup not supported."
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr "Backup status"
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr "Backup status"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -844,6 +844,14 @@ msgstr "Checks if the request's user's password has been changed in the last x d
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr "Chinese (simplified)"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr "Chinese (traditional)"
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr "Clear Flow cache"
|
||||
@ -948,8 +956,12 @@ msgid "Configuration flow"
|
||||
msgstr "Configuration flow"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
msgstr "Configuration stage"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr "Configuration stage"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr "Configuration stages"
|
||||
|
||||
#:
|
||||
#~ msgid "Configure WebAuthn"
|
||||
@ -2205,6 +2217,10 @@ msgstr "Generic"
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr "Generic OpenID Connect"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4483,8 +4499,8 @@ msgid "Stage type"
|
||||
msgstr "Stage type"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4543,6 +4559,10 @@ msgstr "Stages"
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr "State"
|
||||
@ -5014,6 +5034,10 @@ msgstr "TOTP Device"
|
||||
msgid "TOTP authenticator"
|
||||
msgstr "TOTP authenticator"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr "Taiwanese Mandarin"
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr "Target"
|
||||
@ -5673,8 +5697,8 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr "Use the username and password below to authenticate. The password can be retrieved later on the Tokens page."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||
@ -6036,6 +6060,10 @@ msgstr "When enabled, the invitation will be deleted after usage."
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr "When enabled, user fields are matched regardless of their casing."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
|
@ -561,16 +561,16 @@ msgid "Background shown during execution."
|
||||
msgstr "Se muestra el fondo durante la ejecución."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr "La copia de seguridad terminó con errores."
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr "La copia de seguridad terminó con errores."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr "La copia de seguridad finalizó con advertencias o copias de seguridad no compatibles"
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr "La copia de seguridad finalizó con advertencias o copias de seguridad no compatibles"
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr "Estado de respaldo"
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr "Estado de respaldo"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -838,6 +838,14 @@ msgstr "Comprueba si la contraseña del usuario de la solicitud se ha cambiado e
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr "Comprueba el valor de la solicitud de política en relación con varias reglas, que se utilizan principalmente para garantizar la seguridad de la contraseña."
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr "Borrar caché de flujo"
|
||||
@ -942,8 +950,12 @@ msgid "Configuration flow"
|
||||
msgstr "Flujo de configuración"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
msgstr "Etapa de configuración"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr "Etapa de configuración"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Configure WebAuthn"
|
||||
#~ msgstr "Configurar WebAuthn"
|
||||
@ -2166,6 +2178,10 @@ msgstr "Genérico"
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr "Conexión OpenID genérica"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4391,8 +4407,8 @@ msgid "Stage type"
|
||||
msgstr "Tipo de escenario"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario."
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario."
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4451,6 +4467,10 @@ msgstr "Etapas"
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr "Las etapas son pasos individuales de un flujo por los que se guía al usuario. Una etapa solo se puede ejecutar desde dentro de un flujo."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr "Estado"
|
||||
@ -4905,6 +4925,10 @@ msgstr "Dispositivo TOTP"
|
||||
msgid "TOTP authenticator"
|
||||
msgstr "Autenticador TOTP"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr "Objetivo"
|
||||
@ -5556,8 +5580,8 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr "Use el nombre de usuario y la contraseña a continuación para autenticarse. La contraseña se puede recuperar más adelante en la página Tokens."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Use este proveedor con auth_request de nginx o ForwardAuth de traefik. Cada aplicación/dominio necesita su propio proveedor. Además, en cada dominio, /akprox debe enrutarse al puesto avanzado (cuando se usa un puesto avanzado administrado, esto se hace por usted)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Use este proveedor con auth_request de nginx o ForwardAuth de traefik. Cada aplicación/dominio necesita su propio proveedor. Además, en cada dominio, /outpost.goauthentik.io debe enrutarse al puesto avanzado (cuando se usa un puesto avanzado administrado, esto se hace por usted)."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||
@ -5912,6 +5936,10 @@ msgstr "Cuando se habilita, la invitación se eliminará después de su uso."
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr "Cuando se habilita, los campos de usuario coinciden independientemente de su carcasa."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr "Cuando se selecciona, se muestra un campo de contraseña en la misma página en lugar de en una página separada. Esto evita ataques de enumeración de nombres de usuario."
|
||||
|
@ -567,16 +567,16 @@ msgid "Background shown during execution."
|
||||
msgstr "Arrière-plan utilisé durant l'exécution."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr "Sauvegarde terminée avec des erreurs."
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr "Sauvegarde terminée avec des erreurs."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr "Sauvegarde terminée avec avertissements/sauvegarde non supportée."
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr "Sauvegarde terminée avec avertissements/sauvegarde non supportée."
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr "État de la sauvegarde"
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr "État de la sauvegarde"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -846,6 +846,14 @@ msgstr "Vérifie si le mot de passe de l'usager a été changé dans les X derni
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr "Vérifie la valeur de la requête via plusieurs règles, principalement utilisé pour s'assurer de la robustesse des mots de passe."
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr "Vider le cache des flux"
|
||||
@ -950,8 +958,12 @@ msgid "Configuration flow"
|
||||
msgstr "Flux de configuration"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
msgstr "Étape de configuration"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr "Étape de configuration"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
#~ msgid "Configure WebAuthn"
|
||||
@ -2191,6 +2203,10 @@ msgstr ""
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4444,8 +4460,8 @@ msgid "Stage type"
|
||||
msgstr "Type d'étape"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Étape utilisée pour configurer l'Authenticator lorsqu'un utilisateur n'a pas d'appareil compatible. Une fois cette étape franchie, l'utilisateur ne sera plus sollicité."
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr "Étape utilisée pour configurer l'Authenticator lorsqu'un utilisateur n'a pas d'appareil compatible. Une fois cette étape franchie, l'utilisateur ne sera plus sollicité."
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4504,6 +4520,10 @@ msgstr "Étapes"
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr "Les étapes sont des étapes simples d'un flux au travers duquel un utilisateur est guidé. Une étape peut être uniquement exécutée à l'intérieur d'un flux."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr "État"
|
||||
@ -4969,6 +4989,10 @@ msgstr ""
|
||||
msgid "TOTP authenticator"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr "Cible"
|
||||
@ -5614,8 +5638,8 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr "Utilisez le nom d'utilisateur et le mot de passe ci-dessous pour vous authentifier. Le mot de passe peut être récupéré plus tard sur la page Jetons."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Utilisez ce fournisseur avec auth_request de nginx ou forwardAuth de traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, /akprox doit être routé vers l'avant-poste (si vous utilisez un avant-poste géré, cela est fait pour vous)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Utilisez ce fournisseur avec auth_request de nginx ou forwardAuth de traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, /outpost.goauthentik.io doit être routé vers l'avant-poste (si vous utilisez un avant-poste géré, cela est fait pour vous)."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||
@ -5975,6 +5999,10 @@ msgstr "Si activée, l'invitation sera supprimée après utilisation."
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr "Si activé, les champs de l'utilisateur sont mis en correspondance en ignorant leur casse."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr "Si activée, un champ de mot de passe est affiché sur la même page au lieu d'une page séparée. Cela permet d'éviter les attaques par énumération de noms d'utilisateur."
|
||||
|
@ -560,17 +560,14 @@ msgstr "Obraz tła"
|
||||
msgid "Background shown during execution."
|
||||
msgstr "Tło pokazywane podczas wykonywania."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr "Kopia zapasowa zakończona z błędami."
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr "Kopia zapasowa zakończona z błędami."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr "Tworzenie kopii zapasowej zakończone z ostrzeżeniami/kopia zapasowa nie jest obsługiwana."
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr "Tworzenie kopii zapasowej zakończone z ostrzeżeniami/kopia zapasowa nie jest obsługiwana."
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr "Stan kopii zapasowej"
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr "Stan kopii zapasowej"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -838,6 +835,14 @@ msgstr "Sprawdza, czy żądanego użytkownika hasło zostało zmienione w ciągu
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr "Sprawdza wartość z żądania zasad pod kątem kilku reguł, używanych głównie w celu zapewnienia siły hasła."
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr "Chiński (uproszczony)"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr "Chiński (tradycyjny)"
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr "Wyczyść pamięć podręczną przepływu"
|
||||
@ -942,8 +947,12 @@ msgid "Configuration flow"
|
||||
msgstr "Przepływ konfiguracji"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
msgstr "Etap konfiguracji"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr "Etap konfiguracji"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Configure WebAuthn"
|
||||
#~ msgstr "Skonfiguruj WebAuthn"
|
||||
@ -2166,6 +2175,10 @@ msgstr "Ogólny"
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr "Ogólny OpenID Connect"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4391,8 +4404,8 @@ msgid "Stage type"
|
||||
msgstr "Typ etapu"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany."
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany."
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4451,6 +4464,10 @@ msgstr "Etapy"
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr "Etapy to pojedyncze kroki przepływu, przez które prowadzony jest użytkownik. Etap można wykonać tylko z przepływu."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr "Stan"
|
||||
@ -4905,6 +4922,10 @@ msgstr "Urządzenie TOTP"
|
||||
msgid "TOTP authenticator"
|
||||
msgstr "Uwierzytelniacz TOTP"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr "Tajwański mandaryński"
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr "Cel"
|
||||
@ -5556,8 +5577,8 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr "Użyj poniższej nazwy użytkownika i hasła do uwierzytelnienia. Hasło można później odzyskać na stronie Tokeny."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Użyj tego dostawcy z auth_request nginx lub forwardAuth traefik. Każda aplikacja/domena potrzebuje własnego dostawcy. Dodatkowo w każdej domenie /akprox musi być przekierowany do placówki (w przypadku korzystania z zarządzanej placówki jest to zrobione za Ciebie)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Użyj tego dostawcy z auth_request nginx lub forwardAuth traefik. Każda aplikacja/domena potrzebuje własnego dostawcy. Dodatkowo w każdej domenie /outpost.goauthentik.io musi być przekierowany do placówki (w przypadku korzystania z zarządzanej placówki jest to zrobione za Ciebie)."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||
@ -5912,6 +5933,10 @@ msgstr "Po włączeniu zaproszenie zostanie usunięte po użyciu."
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr "Po włączeniu pola użytkownika są dopasowywane niezależnie od wielkości liter."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr "Po wybraniu pole hasła jest wyświetlane na tej samej stronie zamiast na osobnej stronie. Zapobiega to atakom polegającym na wyliczaniu nazw użytkowników."
|
||||
|
6648
web/src/locales/pl_PL.po
Normal file
6648
web/src/locales/pl_PL.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -559,16 +559,16 @@ msgid "Background shown during execution."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr ""
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr ""
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr ""
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -838,6 +838,14 @@ msgstr ""
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr ""
|
||||
@ -942,7 +950,11 @@ msgid "Configuration flow"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
@ -2197,6 +2209,10 @@ msgstr ""
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4473,8 +4489,8 @@ msgid "Stage type"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4533,6 +4549,10 @@ msgstr ""
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
@ -5004,6 +5024,10 @@ msgstr ""
|
||||
msgid "TOTP authenticator"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr ""
|
||||
@ -5653,7 +5677,7 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
@ -6016,6 +6040,10 @@ msgstr ""
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr ""
|
||||
|
@ -561,16 +561,16 @@ msgid "Background shown during execution."
|
||||
msgstr "Yürütme sırasında arka plan gösterilir."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with errors."
|
||||
msgstr "Yedekleme hatalarla tamamlandı."
|
||||
#~ msgid "Backup finished with errors."
|
||||
#~ msgstr "Yedekleme hatalarla tamamlandı."
|
||||
|
||||
#: src/pages/admin-overview/cards/BackupStatusCard.ts
|
||||
msgid "Backup finished with warnings/backup not supported."
|
||||
msgstr "Yedekleme desteklenmeyen uyarılar/yedekleme ile tamamlandı."
|
||||
#~ msgid "Backup finished with warnings/backup not supported."
|
||||
#~ msgstr "Yedekleme desteklenmeyen uyarılar/yedekleme ile tamamlandı."
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "Backup status"
|
||||
msgstr "Yedekleme durumu"
|
||||
#~ msgid "Backup status"
|
||||
#~ msgstr "Yedekleme durumu"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
#: src/pages/providers/ldap/LDAPProviderViewPage.ts
|
||||
@ -838,6 +838,14 @@ msgstr "İsteğin kullanıcı parolasının son x gün içinde değiştirilip de
|
||||
msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength."
|
||||
msgstr "İlke isteğindeki değeri, çoğunlukla parola gücünü sağlamak için kullanılan çeşitli kurallara göre denetler."
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (simplified)"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Clear Flow cache"
|
||||
msgstr "Akış önbelleğini temizleme"
|
||||
@ -942,8 +950,12 @@ msgid "Configuration flow"
|
||||
msgstr "Yapılandırma akışı"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stage"
|
||||
msgstr "Yapılandırma aşamasında"
|
||||
#~ msgid "Configuration stage"
|
||||
#~ msgstr "Yapılandırma aşamasında"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Configuration stages"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Configure WebAuthn"
|
||||
#~ msgstr "WebAuthn'i Yapılandır"
|
||||
@ -2166,6 +2178,10 @@ msgstr "Jenerik"
|
||||
msgid "Generic OpenID Connect"
|
||||
msgstr "Genel OpenID Connect"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
|
||||
msgid "Get this value from https://console.twilio.com"
|
||||
@ -4393,8 +4409,8 @@ msgid "Stage type"
|
||||
msgstr "Aşama türü"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez."
|
||||
#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
#~ msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez."
|
||||
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)."
|
||||
@ -4453,6 +4469,10 @@ msgstr "Aşamalar"
|
||||
msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow."
|
||||
msgstr "Aşamalar, bir Akış'ın kullanıcının yönlendirildiği tek adımlardır. Bir aşama yalnızca bir akış içinden yürütülebilir."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||
msgid "State"
|
||||
msgstr "Eyalet"
|
||||
@ -4907,6 +4927,10 @@ msgstr "TOTP Cihazı"
|
||||
msgid "TOTP authenticator"
|
||||
msgstr "TOTP kimlik doğrulayıcı"
|
||||
|
||||
#: src/interfaces/locale.ts
|
||||
msgid "Taiwanese Mandarin"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Target"
|
||||
msgstr "Hedef"
|
||||
@ -5558,8 +5582,8 @@ msgid "Use the username and password below to authenticate. The password can be
|
||||
msgstr "Kimlik doğrulaması için aşağıdaki kullanıcı adı ve parolayı kullanın. Parola daha sonra Belirteçler sayfasından alınabilir."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Bu sağlayıcıyı nginx'in auth_request veya traefik's forwardAuth ile kullanın. Her uygulama/etki alanının kendi sağlayıcısına ihtiyacı vardır. Ayrıca, her etki alanında /akprox üsse yönlendirilmelidir (manged bir üs kullanırken, bu sizin için yapılır)."
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||
msgstr "Bu sağlayıcıyı nginx'in auth_request veya traefik's forwardAuth ile kullanın. Her uygulama/etki alanının kendi sağlayıcısına ihtiyacı vardır. Ayrıca, her etki alanında /outpost.goauthentik.io üsse yönlendirilmelidir (manged bir üs kullanırken, bu sizin için yapılır)."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||
@ -5914,6 +5938,10 @@ msgstr "Etkinleştirildiğinde, davetiye kullanımdan sonra silinir."
|
||||
msgid "When enabled, user fields are matched regardless of their casing."
|
||||
msgstr "Etkinleştirildiğinde, kullanıcı alanları muhafazası ne olursa olsun eşleştirilir."
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "When multiple stages are selected, the user can choose which one they want to enroll."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
|
||||
msgstr "Seçildiğinde, ayrı bir sayfa yerine aynı sayfada bir parola alanı gösterilir. Bu, kullanıcı adı numaralandırma saldırılarını engeller."
|
||||
|
6117
web/src/locales/zh-Hans.po
Normal file
6117
web/src/locales/zh-Hans.po
Normal file
File diff suppressed because it is too large
Load Diff
6117
web/src/locales/zh-Hant.po
Normal file
6117
web/src/locales/zh-Hant.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user