Compare commits
1 Commits
router-tid
...
flows/conc
Author | SHA1 | Date | |
---|---|---|---|
5797a51993 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2025.2.3
|
current_version = 2025.2.2
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
@ -17,8 +17,6 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:pyproject.toml]
|
[bumpversion:file:pyproject.toml]
|
||||||
|
|
||||||
[bumpversion:file:uv.lock]
|
|
||||||
|
|
||||||
[bumpversion:file:package.json]
|
[bumpversion:file:package.json]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
|
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Documentation issue
|
|
||||||
about: Suggest an improvement or report a problem
|
|
||||||
title: ""
|
|
||||||
labels: documentation
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
|
||||||
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
|
||||||
|
|
||||||
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
|
||||||
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the documentation issue here.
|
|
||||||
|
|
||||||
**Consider opening a PR!**
|
|
||||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
|
@ -44,6 +44,7 @@ if is_release:
|
|||||||
]
|
]
|
||||||
if not prerelease:
|
if not prerelease:
|
||||||
image_tags += [
|
image_tags += [
|
||||||
|
f"{name}:latest",
|
||||||
f"{name}:{version_family}",
|
f"{name}:{version_family}",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v7
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout 5000s --verbose
|
args: --timeout 5000s --verbose
|
||||||
|
27
.github/workflows/semgrep.yml
vendored
27
.github/workflows/semgrep.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: authentik-semgrep
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
pull_request: {}
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- .github/workflows/semgrep.yml
|
|
||||||
schedule:
|
|
||||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
|
||||||
- cron: '12 15 * * *'
|
|
||||||
jobs:
|
|
||||||
semgrep:
|
|
||||||
name: semgrep/ci
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
env:
|
|
||||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
|
||||||
container:
|
|
||||||
image: semgrep/semgrep
|
|
||||||
if: (github.actor != 'dependabot[bot]')
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: semgrep ci
|
|
@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
# Stage 3: Build go proxy
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/authentik ./cmd/server
|
go build -o /go/authentik ./cmd/server
|
||||||
|
|
||||||
# Stage 4: MaxMind GeoIP
|
# Stage 4: MaxMind GeoIP
|
||||||
@ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Download uv
|
# Stage 5: Download uv
|
||||||
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
|
FROM ghcr.io/astral-sh/uv:0.6.8 AS uv
|
||||||
# Stage 6: Base python image
|
# Stage 6: Base python image
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
|
||||||
|
|
||||||
ENV VENV_PATH="/ak-root/.venv" \
|
ENV VENV_PATH="/ak-root/.venv" \
|
||||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2025.2.3"
|
__version__ = "2025.2.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
continue
|
continue
|
||||||
actual_value = value
|
actual_value = value
|
||||||
if raw_session is not None and raw_session in actual_value:
|
if raw_session in actual_value:
|
||||||
actual_value = actual_value.replace(
|
actual_value = actual_value.replace(
|
||||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||||
)
|
)
|
||||||
|
@ -49,8 +49,6 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
"branding_custom_css",
|
|
||||||
"branding_default_flow_background",
|
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
@ -88,7 +86,6 @@ class CurrentBrandSerializer(PassiveSerializer):
|
|||||||
branding_title = CharField()
|
branding_title = CharField()
|
||||||
branding_logo = CharField(source="branding_logo_url")
|
branding_logo = CharField(source="branding_logo_url")
|
||||||
branding_favicon = CharField(source="branding_favicon_url")
|
branding_favicon = CharField(source="branding_favicon_url")
|
||||||
branding_custom_css = CharField()
|
|
||||||
ui_footer_links = ListField(
|
ui_footer_links = ListField(
|
||||||
child=FooterLinkSerializer(),
|
child=FooterLinkSerializer(),
|
||||||
read_only=True,
|
read_only=True,
|
||||||
@ -128,7 +125,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
"branding_default_flow_background",
|
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.0.12 on 2025-02-22 01:51
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.apps.registry import Apps
|
|
||||||
|
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
Brand = apps.get_model("authentik_brands", "brand")
|
|
||||||
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
path = Path("/web/dist/custom.css")
|
|
||||||
if not path.exists():
|
|
||||||
return
|
|
||||||
css = path.read_text()
|
|
||||||
Brand.objects.using(db_alias).update(branding_custom_css=css)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_brands", "0007_brand_default_application"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="brand",
|
|
||||||
name="branding_custom_css",
|
|
||||||
field=models.TextField(blank=True, default=""),
|
|
||||||
),
|
|
||||||
migrations.RunPython(migrate_custom_css),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-19 22:54
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_brands", "0008_brand_branding_custom_css"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="brand",
|
|
||||||
name="branding_default_flow_background",
|
|
||||||
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
|
|
||||||
),
|
|
||||||
]
|
|
@ -33,10 +33,6 @@ class Brand(SerializerModel):
|
|||||||
|
|
||||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
||||||
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
||||||
branding_custom_css = models.TextField(default="", blank=True)
|
|
||||||
branding_default_flow_background = models.TextField(
|
|
||||||
default="/static/dist/assets/images/flow_background.jpg"
|
|
||||||
)
|
|
||||||
|
|
||||||
flow_authentication = models.ForeignKey(
|
flow_authentication = models.ForeignKey(
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
||||||
@ -88,12 +84,6 @@ class Brand(SerializerModel):
|
|||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
||||||
return self.branding_favicon
|
return self.branding_favicon
|
||||||
|
|
||||||
def branding_default_flow_background_url(self) -> str:
|
|
||||||
"""Get branding_default_flow_background with the correct prefix"""
|
|
||||||
if self.branding_default_flow_background.startswith("/static"):
|
|
||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
|
|
||||||
return self.branding_default_flow_background
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.brands.api import BrandSerializer
|
from authentik.brands.api import BrandSerializer
|
||||||
|
@ -24,7 +24,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": brand.domain,
|
"matched_domain": brand.domain,
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -44,7 +43,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "custom",
|
"branding_title": "custom",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": "bar.baz",
|
"matched_domain": "bar.baz",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -61,7 +59,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": "fallback",
|
"matched_domain": "fallback",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -124,27 +121,3 @@ class TestBrands(APITestCase):
|
|||||||
"subject": None,
|
"subject": None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_branding_url(self):
|
|
||||||
"""Test branding attributes return correct values"""
|
|
||||||
brand = create_test_brand()
|
|
||||||
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.branding_logo = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.save()
|
|
||||||
self.assertEqual(
|
|
||||||
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
|
||||||
{
|
|
||||||
"branding_logo": "https://goauthentik.io/img/icon.png",
|
|
||||||
"branding_favicon": "https://goauthentik.io/img/icon.png",
|
|
||||||
"branding_title": "authentik",
|
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": brand.domain,
|
|
||||||
"ui_footer_links": [],
|
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
|
||||||
"default_locale": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -46,7 +46,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||||
"""Cache key where application list for user is saved"""
|
"""Cache key where application list for user is saved"""
|
||||||
key = f"{CACHE_PREFIX}app_access/{user_pk}"
|
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
|
||||||
if page_number:
|
if page_number:
|
||||||
key += f"/{page_number}"
|
key += f"/{page_number}"
|
||||||
return key
|
return key
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from importlib import import_module
|
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
|
|||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
@ -375,7 +373,7 @@ class UsersFilter(FilterSet):
|
|||||||
method="filter_attributes",
|
method="filter_attributes",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||||
uuid = UUIDFilter(field_name="uuid")
|
uuid = UUIDFilter(field_name="uuid")
|
||||||
|
|
||||||
path = CharFilter(field_name="path")
|
path = CharFilter(field_name="path")
|
||||||
@ -393,11 +391,6 @@ class UsersFilter(FilterSet):
|
|||||||
queryset=Group.objects.all().order_by("name"),
|
queryset=Group.objects.all().order_by("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_is_superuser(self, queryset, name, value):
|
|
||||||
if value:
|
|
||||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
|
||||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
|
||||||
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
"""Filter attributes by query args"""
|
"""Filter attributes by query args"""
|
||||||
try:
|
try:
|
||||||
@ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||||
session_ids = sessions.values_list("session_key", flat=True)
|
session_ids = sessions.values_list("session_key", flat=True)
|
||||||
for session in session_ids:
|
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||||
SessionStore(session).delete()
|
|
||||||
sessions.delete()
|
sessions.delete()
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
@ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return ""
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
"""Return property mapping type used by this object"""
|
"""Return property mapping type used by this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
from authentik.core.models import PropertyMapping
|
|
||||||
|
|
||||||
return PropertyMapping
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||||
@ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a user to build final properties upon."""
|
"""Get base properties for a user to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a group to build final properties upon."""
|
"""Get base properties for a group to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
@ -28,7 +25,6 @@ password_changed = Signal()
|
|||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save, sender=Application)
|
||||||
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
SessionStore(instance.session_key).delete()
|
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
@ -36,7 +36,6 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||||
from authentik.lib.utils.urls import is_url_absolute
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
@ -49,7 +48,6 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||||
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
||||||
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
|
|
||||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||||
|
|
||||||
|
|
||||||
@ -210,8 +208,6 @@ class SourceFlowManager:
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
if not is_url_absolute(final_redirect):
|
|
||||||
final_redirect = "authentik_core:if-user"
|
|
||||||
flow_context.update(
|
flow_context.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# Since we authenticate the user by their token, they have no backend set
|
||||||
@ -265,7 +261,6 @@ class SourceFlowManager:
|
|||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
|
|
||||||
return plan.to_redirect(self.request, flow)
|
return plan.to_redirect(self.request, flow)
|
||||||
|
|
||||||
def handle_auth(
|
def handle_auth(
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<style>{{ brand.branding_custom_css }}</style>
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
|
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
|
||||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
from django.apps import apps
|
|
||||||
from django.urls import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
|
|
||||||
|
|
||||||
class TestSourceAPI(APITestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = create_test_admin_user()
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_builtin_source_used_by(self):
|
|
||||||
"""Test Providers's types endpoint"""
|
|
||||||
apps.get_app_config("authentik_core").source_inbuilt()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,7 +1,6 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -16,12 +15,7 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
from authentik.core.tests.utils import (
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
create_test_admin_user,
|
|
||||||
create_test_brand,
|
|
||||||
create_test_flow,
|
|
||||||
create_test_user,
|
|
||||||
)
|
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.admin = create_test_admin_user()
|
self.admin = create_test_admin_user()
|
||||||
self.user = create_test_user()
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
def test_filter_type(self):
|
def test_filter_type(self):
|
||||||
"""Test API filtering by type"""
|
"""Test API filtering by type"""
|
||||||
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_filter_is_superuser(self):
|
|
||||||
"""Test API filtering by superuser status"""
|
|
||||||
User.objects.all().delete()
|
|
||||||
admin = create_test_admin_user()
|
|
||||||
self.client.force_login(admin)
|
|
||||||
# Test superuser
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1)
|
|
||||||
self.assertEqual(body["results"][0]["username"], admin.username)
|
|
||||||
# Test non-superuser
|
|
||||||
user = create_test_user()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1, body)
|
|
||||||
self.assertEqual(body["results"][0]["username"], user.username)
|
|
||||||
|
|
||||||
def test_list_with_groups(self):
|
def test_list_with_groups(self):
|
||||||
"""Test listing with groups"""
|
"""Test listing with groups"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
def test_recovery_email_no_flow(self):
|
def test_recovery_email_no_flow(self):
|
||||||
"""Test user recovery link (no recovery flow set)"""
|
"""Test user recovery link (no recovery flow set)"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
self.user.email = ""
|
|
||||||
self.user.save()
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||||
)
|
)
|
||||||
|
@ -11,14 +11,13 @@ from guardian.shortcuts import get_anonymous_user
|
|||||||
from authentik.core.models import Source, User
|
from authentik.core.models import Source, User
|
||||||
from authentik.core.sources.flow_manager import (
|
from authentik.core.sources.flow_manager import (
|
||||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
||||||
SESSION_KEY_SOURCE_FLOW_CONTEXT,
|
|
||||||
SESSION_KEY_SOURCE_FLOW_STAGES,
|
SESSION_KEY_SOURCE_FLOW_STAGES,
|
||||||
)
|
)
|
||||||
from authentik.core.types import UILoginButton
|
from authentik.core.types import UILoginButton
|
||||||
from authentik.enterprise.stages.source.models import SourceStage
|
from authentik.enterprise.stages.source.models import SourceStage
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||||
from authentik.flows.models import FlowToken, in_memory_stage
|
from authentik.flows.models import FlowToken, in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
|
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||||
from authentik.flows.stage import ChallengeStageView, StageView
|
from authentik.flows.stage import ChallengeStageView, StageView
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
|
||||||
@ -54,9 +53,6 @@ class SourceStageView(ChallengeStageView):
|
|||||||
resume_token = self.create_flow_token()
|
resume_token = self.create_flow_token()
|
||||||
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
||||||
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
|
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
|
||||||
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
|
|
||||||
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
|
|
||||||
}
|
|
||||||
return self.login_button.challenge
|
return self.login_button.challenge
|
||||||
|
|
||||||
def create_flow_token(self) -> FlowToken:
|
def create_flow_token(self) -> FlowToken:
|
||||||
|
@ -50,8 +50,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
"mode",
|
"mode",
|
||||||
"mode_verbose",
|
"mode_verbose",
|
||||||
"webhook_url",
|
"webhook_url",
|
||||||
"webhook_mapping_body",
|
"webhook_mapping",
|
||||||
"webhook_mapping_headers",
|
|
||||||
"send_once",
|
"send_once",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-20 19:54
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="notificationtransport",
|
|
||||||
old_name="webhook_mapping",
|
|
||||||
new_name="webhook_mapping_body",
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="notificationtransport",
|
|
||||||
name="webhook_mapping_body",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
|
||||||
related_name="+",
|
|
||||||
to="authentik_events.notificationwebhookmapping",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="notificationtransport",
|
|
||||||
name="webhook_mapping_headers",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
|
||||||
related_name="+",
|
|
||||||
to="authentik_events.notificationwebhookmapping",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -336,27 +336,8 @@ class NotificationTransport(SerializerModel):
|
|||||||
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
|
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
|
||||||
|
|
||||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||||
webhook_mapping_body = models.ForeignKey(
|
webhook_mapping = models.ForeignKey(
|
||||||
"NotificationWebhookMapping",
|
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||||
on_delete=models.SET_DEFAULT,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
related_name="+",
|
|
||||||
help_text=_(
|
|
||||||
"Customize the body of the request. "
|
|
||||||
"Mapping should return data that is JSON-serializable."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
webhook_mapping_headers = models.ForeignKey(
|
|
||||||
"NotificationWebhookMapping",
|
|
||||||
on_delete=models.SET_DEFAULT,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
related_name="+",
|
|
||||||
help_text=_(
|
|
||||||
"Configure additional headers to be sent. "
|
|
||||||
"Mapping should return a dictionary of key-value pairs"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
send_once = models.BooleanField(
|
send_once = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@ -379,8 +360,8 @@ class NotificationTransport(SerializerModel):
|
|||||||
|
|
||||||
def send_local(self, notification: "Notification") -> list[str]:
|
def send_local(self, notification: "Notification") -> list[str]:
|
||||||
"""Local notification delivery"""
|
"""Local notification delivery"""
|
||||||
if self.webhook_mapping_body:
|
if self.webhook_mapping:
|
||||||
self.webhook_mapping_body.evaluate(
|
self.webhook_mapping.evaluate(
|
||||||
user=notification.user,
|
user=notification.user,
|
||||||
request=None,
|
request=None,
|
||||||
notification=notification,
|
notification=notification,
|
||||||
@ -399,18 +380,9 @@ class NotificationTransport(SerializerModel):
|
|||||||
if notification.event and notification.event.user:
|
if notification.event and notification.event.user:
|
||||||
default_body["event_user_email"] = notification.event.user.get("email", None)
|
default_body["event_user_email"] = notification.event.user.get("email", None)
|
||||||
default_body["event_user_username"] = notification.event.user.get("username", None)
|
default_body["event_user_username"] = notification.event.user.get("username", None)
|
||||||
headers = {}
|
if self.webhook_mapping:
|
||||||
if self.webhook_mapping_body:
|
|
||||||
default_body = sanitize_item(
|
default_body = sanitize_item(
|
||||||
self.webhook_mapping_body.evaluate(
|
self.webhook_mapping.evaluate(
|
||||||
user=notification.user,
|
|
||||||
request=None,
|
|
||||||
notification=notification,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if self.webhook_mapping_headers:
|
|
||||||
headers = sanitize_item(
|
|
||||||
self.webhook_mapping_headers.evaluate(
|
|
||||||
user=notification.user,
|
user=notification.user,
|
||||||
request=None,
|
request=None,
|
||||||
notification=notification,
|
notification=notification,
|
||||||
@ -420,7 +392,6 @@ class NotificationTransport(SerializerModel):
|
|||||||
response = get_http_session().post(
|
response = get_http_session().post(
|
||||||
self.webhook_url,
|
self.webhook_url,
|
||||||
json=default_body,
|
json=default_body,
|
||||||
headers=headers,
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
|
@ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
transport = NotificationTransport.objects.create(
|
transport = NotificationTransport.objects.create(
|
||||||
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL
|
name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL
|
||||||
)
|
)
|
||||||
NotificationRule.objects.filter(name__startswith="default").delete()
|
NotificationRule.objects.filter(name__startswith="default").delete()
|
||||||
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
|
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
|
||||||
|
@ -60,25 +60,20 @@ class TestEventTransports(TestCase):
|
|||||||
|
|
||||||
def test_transport_webhook_mapping(self):
|
def test_transport_webhook_mapping(self):
|
||||||
"""Test webhook transport with custom mapping"""
|
"""Test webhook transport with custom mapping"""
|
||||||
mapping_body = NotificationWebhookMapping.objects.create(
|
mapping = NotificationWebhookMapping.objects.create(
|
||||||
name=generate_id(), expression="return request.user"
|
name=generate_id(), expression="return request.user"
|
||||||
)
|
)
|
||||||
mapping_headers = NotificationWebhookMapping.objects.create(
|
|
||||||
name=generate_id(), expression="""return {"foo": "bar"}"""
|
|
||||||
)
|
|
||||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
mode=TransportMode.WEBHOOK,
|
mode=TransportMode.WEBHOOK,
|
||||||
webhook_url="http://localhost:1234/test",
|
webhook_url="http://localhost:1234/test",
|
||||||
webhook_mapping_body=mapping_body,
|
webhook_mapping=mapping,
|
||||||
webhook_mapping_headers=mapping_headers,
|
|
||||||
)
|
)
|
||||||
with Mocker() as mocker:
|
with Mocker() as mocker:
|
||||||
mocker.post("http://localhost:1234/test")
|
mocker.post("http://localhost:1234/test")
|
||||||
transport.send(self.notification)
|
transport.send(self.notification)
|
||||||
self.assertEqual(mocker.call_count, 1)
|
self.assertEqual(mocker.call_count, 1)
|
||||||
self.assertEqual(mocker.request_history[0].method, "POST")
|
self.assertEqual(mocker.request_history[0].method, "POST")
|
||||||
self.assertEqual(mocker.request_history[0].headers["foo"], "bar")
|
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
mocker.request_history[0].body.decode(),
|
mocker.request_history[0].body.decode(),
|
||||||
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username},
|
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username},
|
||||||
|
@ -54,6 +54,7 @@ class Challenge(PassiveSerializer):
|
|||||||
|
|
||||||
flow_info = ContextualFlowInfo(required=False)
|
flow_info = ContextualFlowInfo(required=False)
|
||||||
component = CharField(default="")
|
component = CharField(default="")
|
||||||
|
xid = CharField(required=False)
|
||||||
|
|
||||||
response_errors = DictField(
|
response_errors = DictField(
|
||||||
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
|
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
|
||||||
|
@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
@ -179,12 +178,11 @@ class Flow(SerializerModel, PolicyBindingModel):
|
|||||||
help_text=_("Required level of authentication and authorization to access a flow."),
|
help_text=_("Required level of authentication and authorization to access a flow."),
|
||||||
)
|
)
|
||||||
|
|
||||||
def background_url(self, request: HttpRequest | None = None) -> str:
|
@property
|
||||||
|
def background_url(self) -> str:
|
||||||
"""Get the URL to the background image. If the name is /static or starts with http
|
"""Get the URL to the background image. If the name is /static or starts with http
|
||||||
it is returned as-is"""
|
it is returned as-is"""
|
||||||
if not self.background:
|
if not self.background:
|
||||||
if request:
|
|
||||||
return request.brand.branding_default_flow_background_url()
|
|
||||||
return (
|
return (
|
||||||
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
|
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
|
||||||
)
|
)
|
||||||
|
@ -143,10 +143,12 @@ class FlowPlan:
|
|||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
flow: Flow,
|
flow: Flow,
|
||||||
allowed_silent_types: list["StageView"] | None = None,
|
allowed_silent_types: list["StageView"] | None = None,
|
||||||
|
**get_params,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
"""Redirect to the flow executor for this flow plan"""
|
"""Redirect to the flow executor for this flow plan"""
|
||||||
from authentik.flows.views.executor import (
|
from authentik.flows.views.executor import (
|
||||||
SESSION_KEY_PLAN,
|
SESSION_KEY_PLAN,
|
||||||
|
FlowContainer,
|
||||||
FlowExecutorView,
|
FlowExecutorView,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,6 +159,7 @@ class FlowPlan:
|
|||||||
# No unskippable stages found, so we can directly return the response of the last stage
|
# No unskippable stages found, so we can directly return the response of the last stage
|
||||||
final_stage: type[StageView] = self.bindings[-1].stage.view
|
final_stage: type[StageView] = self.bindings[-1].stage.view
|
||||||
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
|
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
|
||||||
|
temp_exec.container = FlowContainer(request)
|
||||||
temp_exec.current_stage = self.bindings[-1].stage
|
temp_exec.current_stage = self.bindings[-1].stage
|
||||||
temp_exec.current_stage_view = final_stage
|
temp_exec.current_stage_view = final_stage
|
||||||
temp_exec.setup(request, flow.slug)
|
temp_exec.setup(request, flow.slug)
|
||||||
@ -174,6 +177,9 @@ class FlowPlan:
|
|||||||
):
|
):
|
||||||
get_qs["inspector"] = "available"
|
get_qs["inspector"] = "available"
|
||||||
|
|
||||||
|
for key, value in get_params:
|
||||||
|
get_qs[key] = value
|
||||||
|
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
get_qs,
|
get_qs,
|
||||||
|
@ -184,13 +184,14 @@ class ChallengeStageView(StageView):
|
|||||||
flow_info = ContextualFlowInfo(
|
flow_info = ContextualFlowInfo(
|
||||||
data={
|
data={
|
||||||
"title": self.format_title(),
|
"title": self.format_title(),
|
||||||
"background": self.executor.flow.background_url(self.request),
|
"background": self.executor.flow.background_url,
|
||||||
"cancel_url": reverse("authentik_flows:cancel"),
|
"cancel_url": reverse("authentik_flows:cancel"),
|
||||||
"layout": self.executor.flow.layout,
|
"layout": self.executor.flow.layout,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
flow_info.is_valid()
|
flow_info.is_valid()
|
||||||
challenge.initial_data["flow_info"] = flow_info.data
|
challenge.initial_data["flow_info"] = flow_info.data
|
||||||
|
challenge.initial_data["xid"] = self.executor.container.exec_id
|
||||||
if isinstance(challenge, WithUserInfoChallenge):
|
if isinstance(challenge, WithUserInfoChallenge):
|
||||||
# If there's a pending user, update the `username` field
|
# If there's a pending user, update the `username` field
|
||||||
# this field is only used by password managers.
|
# this field is only used by password managers.
|
||||||
|
@ -28,7 +28,7 @@ window.authentik.flow = {
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-flow-executor flowSlug="{{ flow.slug }}">
|
<ak-flow-executor flowSlug="{{ flow.slug }}" xid="{{ xid }}">
|
||||||
<ak-loading></ak-loading>
|
<ak-loading></ak-loading>
|
||||||
</ak-flow-executor>
|
</ak-flow-executor>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -27,6 +27,7 @@ class FlowTestCase(APITestCase):
|
|||||||
self.assertIsNotNone(raw_response["component"])
|
self.assertIsNotNone(raw_response["component"])
|
||||||
if flow:
|
if flow:
|
||||||
self.assertIn("flow_info", raw_response)
|
self.assertIn("flow_info", raw_response)
|
||||||
|
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
|
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
"""API flow tests"""
|
"""API flow tests"""
|
||||||
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
@ -79,22 +77,6 @@ class TestFlowsAPI(APITestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED})
|
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED})
|
||||||
|
|
||||||
def test_api_background(self):
|
|
||||||
"""Test custom background"""
|
|
||||||
user = create_test_admin_user()
|
|
||||||
self.client.force_login(user)
|
|
||||||
|
|
||||||
flow = create_test_flow()
|
|
||||||
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertEqual(body["background"], "/static/dist/assets/images/flow_background.jpg")
|
|
||||||
|
|
||||||
flow.background = "https://goauthentik.io/img/icon.png"
|
|
||||||
flow.save()
|
|
||||||
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertEqual(body["background"], "https://goauthentik.io/img/icon.png")
|
|
||||||
|
|
||||||
def test_api_diagram_no_stages(self):
|
def test_api_diagram_no_stages(self):
|
||||||
"""Test flow diagram with no stages."""
|
"""Test flow diagram with no stages."""
|
||||||
user = create_test_admin_user()
|
user = create_test_admin_user()
|
||||||
|
@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
|
|||||||
"captcha_stage": None,
|
"captcha_stage": None,
|
||||||
"component": "ak-stage-identification",
|
"component": "ak-stage-identification",
|
||||||
"flow_info": {
|
"flow_info": {
|
||||||
"background": "/static/dist/assets/images/flow_background.jpg",
|
"background": flow.background_url,
|
||||||
"cancel_url": reverse("authentik_flows:cancel"),
|
"cancel_url": reverse("authentik_flows:cancel"),
|
||||||
"title": flow.title,
|
"title": flow.title,
|
||||||
"layout": "stacked",
|
"layout": "stacked",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""authentik multi-stage authentication engine"""
|
"""authentik multi-stage authentication engine"""
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
@ -64,14 +65,15 @@ from authentik.policies.engine import PolicyEngine
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
NEXT_ARG_NAME = "next"
|
NEXT_ARG_NAME = "next"
|
||||||
|
SESSION_KEY_PLAN_CONTAINER = "authentik/flows/plan_container/%s"
|
||||||
SESSION_KEY_PLAN = "authentik/flows/plan"
|
SESSION_KEY_PLAN = "authentik/flows/plan"
|
||||||
SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
||||||
SESSION_KEY_GET = "authentik/flows/get"
|
SESSION_KEY_GET = "authentik/flows/get"
|
||||||
SESSION_KEY_POST = "authentik/flows/post"
|
SESSION_KEY_POST = "authentik/flows/post"
|
||||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||||
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
|
|
||||||
QS_KEY_TOKEN = "flow_token" # nosec
|
QS_KEY_TOKEN = "flow_token" # nosec
|
||||||
QS_QUERY = "query"
|
QS_QUERY = "query"
|
||||||
|
QS_EXEC_ID = "xid"
|
||||||
|
|
||||||
|
|
||||||
def challenge_types():
|
def challenge_types():
|
||||||
@ -98,6 +100,88 @@ class InvalidStageError(SentryIgnoredException):
|
|||||||
"""Error raised when a challenge from a stage is not valid"""
|
"""Error raised when a challenge from a stage is not valid"""
|
||||||
|
|
||||||
|
|
||||||
|
class FlowContainer:
|
||||||
|
"""Allow for multiple concurrent flow executions in the same session"""
|
||||||
|
|
||||||
|
def __init__(self, request: HttpRequest, exec_id: str | None = None) -> None:
|
||||||
|
self.request = request
|
||||||
|
self.exec_id = exec_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def new(request: HttpRequest):
|
||||||
|
exec_id = str(uuid4())
|
||||||
|
request.session[SESSION_KEY_PLAN_CONTAINER % exec_id] = {}
|
||||||
|
return FlowContainer(request, exec_id)
|
||||||
|
|
||||||
|
def exists(self) -> bool:
|
||||||
|
"""Check if flow exists in container/session"""
|
||||||
|
return SESSION_KEY_PLAN in self.session
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.request.session.modified = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
# Backwards compatibility: store session plan/etc directly in session
|
||||||
|
if not self.exec_id:
|
||||||
|
return self.request.session
|
||||||
|
self.request.session.setdefault(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
|
||||||
|
return self.request.session.get(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plan(self) -> FlowPlan:
|
||||||
|
return self.session.get(SESSION_KEY_PLAN)
|
||||||
|
|
||||||
|
def to_redirect(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
flow: Flow,
|
||||||
|
allowed_silent_types: list[StageView] | None = None,
|
||||||
|
**get_params,
|
||||||
|
) -> HttpResponse:
|
||||||
|
get_params[QS_EXEC_ID] = self.exec_id
|
||||||
|
return self.plan.to_redirect(
|
||||||
|
request, flow, allowed_silent_types=allowed_silent_types, **get_params
|
||||||
|
)
|
||||||
|
|
||||||
|
@plan.setter
|
||||||
|
def plan(self, value: FlowPlan):
|
||||||
|
self.session[SESSION_KEY_PLAN] = value
|
||||||
|
self.request.session.modified = True
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def application_pre(self):
|
||||||
|
return self.session.get(SESSION_KEY_APPLICATION_PRE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get(self) -> QueryDict:
|
||||||
|
return self.session.get(SESSION_KEY_GET)
|
||||||
|
|
||||||
|
@get.setter
|
||||||
|
def get(self, value: QueryDict):
|
||||||
|
self.session[SESSION_KEY_GET] = value
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def post(self) -> QueryDict:
|
||||||
|
return self.session.get(SESSION_KEY_POST)
|
||||||
|
|
||||||
|
@post.setter
|
||||||
|
def post(self, value: QueryDict):
|
||||||
|
self.session[SESSION_KEY_POST] = value
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def history(self) -> list[FlowPlan]:
|
||||||
|
return self.session.get(SESSION_KEY_HISTORY)
|
||||||
|
|
||||||
|
@history.setter
|
||||||
|
def history(self, value: list[FlowPlan]):
|
||||||
|
self.session[SESSION_KEY_HISTORY] = value
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||||
class FlowExecutorView(APIView):
|
class FlowExecutorView(APIView):
|
||||||
"""Flow executor, passing requests to Stage Views"""
|
"""Flow executor, passing requests to Stage Views"""
|
||||||
@ -105,8 +189,9 @@ class FlowExecutorView(APIView):
|
|||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
flow: Flow = None
|
flow: Flow = None
|
||||||
|
|
||||||
plan: FlowPlan | None = None
|
plan: FlowPlan | None = None
|
||||||
|
container: FlowContainer
|
||||||
|
|
||||||
current_binding: FlowStageBinding | None = None
|
current_binding: FlowStageBinding | None = None
|
||||||
current_stage: Stage
|
current_stage: Stage
|
||||||
current_stage_view: View
|
current_stage_view: View
|
||||||
@ -161,10 +246,12 @@ class FlowExecutorView(APIView):
|
|||||||
if QS_KEY_TOKEN in get_params:
|
if QS_KEY_TOKEN in get_params:
|
||||||
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
|
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
|
||||||
if plan:
|
if plan:
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
container = FlowContainer.new(request)
|
||||||
|
container.plan = plan
|
||||||
# Early check if there's an active Plan for the current session
|
# Early check if there's an active Plan for the current session
|
||||||
if SESSION_KEY_PLAN in self.request.session:
|
self.container = FlowContainer(request, request.GET.get(QS_EXEC_ID))
|
||||||
self.plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
if self.container.exists():
|
||||||
|
self.plan: FlowPlan = self.container.plan
|
||||||
if self.plan.flow_pk != self.flow.pk.hex:
|
if self.plan.flow_pk != self.flow.pk.hex:
|
||||||
self._logger.warning(
|
self._logger.warning(
|
||||||
"f(exec): Found existing plan for other flow, deleting plan",
|
"f(exec): Found existing plan for other flow, deleting plan",
|
||||||
@ -177,13 +264,14 @@ class FlowExecutorView(APIView):
|
|||||||
self._logger.debug("f(exec): Continuing existing plan")
|
self._logger.debug("f(exec): Continuing existing plan")
|
||||||
|
|
||||||
# Initial flow request, check if we have an upstream query string passed in
|
# Initial flow request, check if we have an upstream query string passed in
|
||||||
request.session[SESSION_KEY_GET] = get_params
|
self.container.get = get_params
|
||||||
# Don't check session again as we've either already loaded the plan or we need to plan
|
# Don't check session again as we've either already loaded the plan or we need to plan
|
||||||
if not self.plan:
|
if not self.plan:
|
||||||
request.session[SESSION_KEY_HISTORY] = []
|
self.container.history = []
|
||||||
self._logger.debug("f(exec): No active Plan found, initiating planner")
|
self._logger.debug("f(exec): No active Plan found, initiating planner")
|
||||||
try:
|
try:
|
||||||
self.plan = self._initiate_plan()
|
self.plan = self._initiate_plan()
|
||||||
|
self.container.plan = self.plan
|
||||||
except FlowNonApplicableException as exc:
|
except FlowNonApplicableException as exc:
|
||||||
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
|
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
|
||||||
return self.handle_invalid_flow(exc)
|
return self.handle_invalid_flow(exc)
|
||||||
@ -255,12 +343,19 @@ class FlowExecutorView(APIView):
|
|||||||
request=OpenApiTypes.NONE,
|
request=OpenApiTypes.NONE,
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="query",
|
name=QS_QUERY,
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
required=True,
|
required=True,
|
||||||
description="Querystring as received",
|
description="Querystring as received",
|
||||||
type=OpenApiTypes.STR,
|
type=OpenApiTypes.STR,
|
||||||
)
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name=QS_EXEC_ID,
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
required=False,
|
||||||
|
description="Flow execution ID",
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
operation_id="flows_executor_get",
|
operation_id="flows_executor_get",
|
||||||
)
|
)
|
||||||
@ -287,7 +382,7 @@ class FlowExecutorView(APIView):
|
|||||||
span.set_data("authentik Stage", self.current_stage_view)
|
span.set_data("authentik Stage", self.current_stage_view)
|
||||||
span.set_data("authentik Flow", self.flow.slug)
|
span.set_data("authentik Flow", self.flow.slug)
|
||||||
stage_response = self.current_stage_view.dispatch(request)
|
stage_response = self.current_stage_view.dispatch(request)
|
||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response, self.container.exec_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return self.handle_exception(exc)
|
return self.handle_exception(exc)
|
||||||
|
|
||||||
@ -306,12 +401,19 @@ class FlowExecutorView(APIView):
|
|||||||
),
|
),
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="query",
|
name=QS_QUERY,
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
required=True,
|
required=True,
|
||||||
description="Querystring as received",
|
description="Querystring as received",
|
||||||
type=OpenApiTypes.STR,
|
type=OpenApiTypes.STR,
|
||||||
)
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name=QS_EXEC_ID,
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
required=True,
|
||||||
|
description="Flow execution ID",
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
operation_id="flows_executor_solve",
|
operation_id="flows_executor_solve",
|
||||||
)
|
)
|
||||||
@ -338,14 +440,15 @@ class FlowExecutorView(APIView):
|
|||||||
span.set_data("authentik Stage", self.current_stage_view)
|
span.set_data("authentik Stage", self.current_stage_view)
|
||||||
span.set_data("authentik Flow", self.flow.slug)
|
span.set_data("authentik Flow", self.flow.slug)
|
||||||
stage_response = self.current_stage_view.dispatch(request)
|
stage_response = self.current_stage_view.dispatch(request)
|
||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response, self.container.exec_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return self.handle_exception(exc)
|
return self.handle_exception(exc)
|
||||||
|
|
||||||
def _initiate_plan(self) -> FlowPlan:
|
def _initiate_plan(self) -> FlowPlan:
|
||||||
planner = FlowPlanner(self.flow)
|
planner = FlowPlanner(self.flow)
|
||||||
plan = planner.plan(self.request)
|
plan = planner.plan(self.request)
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
container = FlowContainer.new(self.request)
|
||||||
|
container.plan = plan
|
||||||
try:
|
try:
|
||||||
# Call the has_stages getter to check that
|
# Call the has_stages getter to check that
|
||||||
# there are no issues with the class we might've gotten
|
# there are no issues with the class we might've gotten
|
||||||
@ -369,7 +472,7 @@ class FlowExecutorView(APIView):
|
|||||||
except FlowNonApplicableException as exc:
|
except FlowNonApplicableException as exc:
|
||||||
self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc)
|
self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc)
|
||||||
return self.handle_invalid_flow(exc)
|
return self.handle_invalid_flow(exc)
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.container.plan = plan
|
||||||
kwargs = self.kwargs
|
kwargs = self.kwargs
|
||||||
kwargs.update({"flow_slug": self.flow.slug})
|
kwargs.update({"flow_slug": self.flow.slug})
|
||||||
return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
|
return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
|
||||||
@ -391,9 +494,13 @@ class FlowExecutorView(APIView):
|
|||||||
)
|
)
|
||||||
self.cancel()
|
self.cancel()
|
||||||
if next_param and not is_url_absolute(next_param):
|
if next_param and not is_url_absolute(next_param):
|
||||||
return to_stage_response(self.request, redirect_with_qs(next_param))
|
return to_stage_response(
|
||||||
|
self.request, redirect_with_qs(next_param), self.container.exec_id
|
||||||
|
)
|
||||||
return to_stage_response(
|
return to_stage_response(
|
||||||
self.request, self.stage_invalid(error_message=_("Invalid next URL"))
|
self.request,
|
||||||
|
self.stage_invalid(error_message=_("Invalid next URL")),
|
||||||
|
self.container.exec_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def stage_ok(self) -> HttpResponse:
|
def stage_ok(self) -> HttpResponse:
|
||||||
@ -407,7 +514,7 @@ class FlowExecutorView(APIView):
|
|||||||
self.current_stage_view.cleanup()
|
self.current_stage_view.cleanup()
|
||||||
self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
|
self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
|
||||||
self.plan.pop()
|
self.plan.pop()
|
||||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
self.container.plan = self.plan
|
||||||
if self.plan.bindings:
|
if self.plan.bindings:
|
||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
"f(exec): Continuing with next stage",
|
"f(exec): Continuing with next stage",
|
||||||
@ -450,11 +557,11 @@ class FlowExecutorView(APIView):
|
|||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel current flow execution"""
|
"""Cancel current flow execution"""
|
||||||
|
# TODO: Clean up container
|
||||||
keys_to_delete = [
|
keys_to_delete = [
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
SESSION_KEY_APPLICATION_PRE,
|
||||||
SESSION_KEY_PLAN,
|
SESSION_KEY_PLAN,
|
||||||
SESSION_KEY_GET,
|
SESSION_KEY_GET,
|
||||||
SESSION_KEY_AUTH_STARTED,
|
|
||||||
# We might need the initial POST payloads for later requests
|
# We might need the initial POST payloads for later requests
|
||||||
# SESSION_KEY_POST,
|
# SESSION_KEY_POST,
|
||||||
# We don't delete the history on purpose, as a user might
|
# We don't delete the history on purpose, as a user might
|
||||||
@ -473,8 +580,8 @@ class CancelView(View):
|
|||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""View which canels the currently active plan"""
|
"""View which canels the currently active plan"""
|
||||||
if SESSION_KEY_PLAN in request.session:
|
if FlowContainer(request, request.GET.get(QS_EXEC_ID)).exists():
|
||||||
del request.session[SESSION_KEY_PLAN]
|
del request.session[SESSION_KEY_PLAN_CONTAINER % request.GET.get(QS_EXEC_ID)]
|
||||||
LOGGER.debug("Canceled current plan")
|
LOGGER.debug("Canceled current plan")
|
||||||
return redirect("authentik_flows:default-invalidation")
|
return redirect("authentik_flows:default-invalidation")
|
||||||
|
|
||||||
@ -522,19 +629,12 @@ class ToDefaultFlow(View):
|
|||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
flow = self.get_flow()
|
flow = self.get_flow()
|
||||||
# If user already has a pending plan, clear it so we don't have to later.
|
get_qs = request.GET.copy()
|
||||||
if SESSION_KEY_PLAN in self.request.session:
|
get_qs[QS_EXEC_ID] = str(uuid4())
|
||||||
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
return redirect_with_qs("authentik_core:if-flow", get_qs, flow_slug=flow.slug)
|
||||||
if plan.flow_pk != flow.pk.hex:
|
|
||||||
LOGGER.warning(
|
|
||||||
"f(def): Found existing plan for other flow, deleting plan",
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
)
|
|
||||||
del self.request.session[SESSION_KEY_PLAN]
|
|
||||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
|
||||||
|
|
||||||
|
|
||||||
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> HttpResponse:
|
||||||
"""Convert normal HttpResponse into JSON Response"""
|
"""Convert normal HttpResponse into JSON Response"""
|
||||||
if (
|
if (
|
||||||
isinstance(source, HttpResponseRedirect)
|
isinstance(source, HttpResponseRedirect)
|
||||||
@ -553,6 +653,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
|||||||
RedirectChallenge(
|
RedirectChallenge(
|
||||||
{
|
{
|
||||||
"to": str(redirect_url),
|
"to": str(redirect_url),
|
||||||
|
"xid": xid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -561,6 +662,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
|||||||
ShellChallenge(
|
ShellChallenge(
|
||||||
{
|
{
|
||||||
"body": source.render().content.decode("utf-8"),
|
"body": source.render().content.decode("utf-8"),
|
||||||
|
"xid": xid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -570,6 +672,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
|||||||
ShellChallenge(
|
ShellChallenge(
|
||||||
{
|
{
|
||||||
"body": source.content.decode("utf-8"),
|
"body": source.content.decode("utf-8"),
|
||||||
|
"xid": xid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -601,4 +704,6 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
|
|||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
LOGGER.warning("Flow not applicable to user")
|
LOGGER.warning("Flow not applicable to user")
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
return plan.to_redirect(request, stage.configure_flow)
|
container = FlowContainer.new(request)
|
||||||
|
container.plan = plan
|
||||||
|
return container.to_redirect(request, stage.configure_flow)
|
||||||
|
@ -6,23 +6,17 @@ from django.shortcuts import get_object_or_404
|
|||||||
from ua_parser.user_agent_parser import Parse
|
from ua_parser.user_agent_parser import Parse
|
||||||
|
|
||||||
from authentik.core.views.interface import InterfaceView
|
from authentik.core.views.interface import InterfaceView
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
|
from authentik.flows.views.executor import QS_EXEC_ID
|
||||||
|
|
||||||
|
|
||||||
class FlowInterfaceView(InterfaceView):
|
class FlowInterfaceView(InterfaceView):
|
||||||
"""Flow interface"""
|
"""Flow interface"""
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
kwargs["flow"] = flow
|
|
||||||
if (
|
|
||||||
not self.request.user.is_authenticated
|
|
||||||
and flow.designation == FlowDesignation.AUTHENTICATION
|
|
||||||
):
|
|
||||||
self.request.session[SESSION_KEY_AUTH_STARTED] = True
|
|
||||||
self.request.session.save()
|
|
||||||
kwargs["inspector"] = "inspector" in self.request.GET
|
kwargs["inspector"] = "inspector" in self.request.GET
|
||||||
|
kwargs["xid"] = self.request.GET.get(QS_EXEC_ID)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
def compat_needs_sfe(self) -> bool:
|
def compat_needs_sfe(self) -> bool:
|
||||||
|
@ -1,20 +1,5 @@
|
|||||||
# authentik configuration
|
# update website/docs/install-config/configuration/configuration.mdx
|
||||||
#
|
# This is the default configuration file
|
||||||
# https://docs.goauthentik.io/docs/install-config/configuration/
|
|
||||||
#
|
|
||||||
# To override the settings in this file, run the following command from the repository root:
|
|
||||||
#
|
|
||||||
# ```shell
|
|
||||||
# make gen-dev-config
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# You may edit the generated file to override the configuration below.
|
|
||||||
#
|
|
||||||
# When making modifying the default configuration file,
|
|
||||||
# ensure that the corresponding documentation is updated to match.
|
|
||||||
#
|
|
||||||
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
|
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
host: localhost
|
host: localhost
|
||||||
name: authentik
|
name: authentik
|
||||||
@ -60,8 +45,6 @@ redis:
|
|||||||
# url: ""
|
# url: ""
|
||||||
# transport_options: ""
|
# transport_options: ""
|
||||||
|
|
||||||
http_timeout: 30
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
# url: ""
|
# url: ""
|
||||||
timeout: 300
|
timeout: 300
|
||||||
|
@ -18,15 +18,6 @@ class SerializerModel(models.Model):
|
|||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
"""Get serializer for this model"""
|
"""Get serializer for this model"""
|
||||||
# Special handling for built-in source
|
|
||||||
if (
|
|
||||||
hasattr(self, "managed")
|
|
||||||
and hasattr(self, "MANAGED_INBUILT")
|
|
||||||
and self.managed == self.MANAGED_INBUILT
|
|
||||||
):
|
|
||||||
from authentik.core.api.sources import SourceSerializer
|
|
||||||
|
|
||||||
return SourceSerializer
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,40 +16,7 @@ def authentik_user_agent() -> str:
|
|||||||
return f"authentik@{get_full_version()}"
|
return f"authentik@{get_full_version()}"
|
||||||
|
|
||||||
|
|
||||||
class TimeoutSession(Session):
|
class DebugSession(Session):
|
||||||
"""Always set a default HTTP request timeout"""
|
|
||||||
|
|
||||||
def __init__(self, default_timeout=None):
|
|
||||||
super().__init__()
|
|
||||||
self.timeout = default_timeout
|
|
||||||
|
|
||||||
def send(
|
|
||||||
self,
|
|
||||||
request,
|
|
||||||
*,
|
|
||||||
stream=...,
|
|
||||||
verify=...,
|
|
||||||
proxies=...,
|
|
||||||
cert=...,
|
|
||||||
timeout=...,
|
|
||||||
allow_redirects=...,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if not timeout and self.timeout:
|
|
||||||
timeout = self.timeout
|
|
||||||
return super().send(
|
|
||||||
request,
|
|
||||||
stream=stream,
|
|
||||||
verify=verify,
|
|
||||||
proxies=proxies,
|
|
||||||
cert=cert,
|
|
||||||
timeout=timeout,
|
|
||||||
allow_redirects=allow_redirects,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DebugSession(TimeoutSession):
|
|
||||||
"""requests session which logs http requests and responses"""
|
"""requests session which logs http requests and responses"""
|
||||||
|
|
||||||
def send(self, req: PreparedRequest, *args, **kwargs):
|
def send(self, req: PreparedRequest, *args, **kwargs):
|
||||||
@ -75,9 +42,8 @@ class DebugSession(TimeoutSession):
|
|||||||
|
|
||||||
def get_http_session() -> Session:
|
def get_http_session() -> Session:
|
||||||
"""Get a requests session with common headers"""
|
"""Get a requests session with common headers"""
|
||||||
session = TimeoutSession()
|
session = Session()
|
||||||
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
|
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
|
||||||
session = DebugSession()
|
session = DebugSession()
|
||||||
session.headers["User-Agent"] = authentik_user_agent()
|
session.headers["User-Agent"] = authentik_user_agent()
|
||||||
session.timeout = CONFIG.get_optional_int("http_timeout")
|
|
||||||
return session
|
return session
|
||||||
|
@ -13,7 +13,6 @@ from paramiko.ssh_exception import SSHException
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
from authentik import __version__
|
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||||
@ -185,7 +184,7 @@ class DockerController(BaseController):
|
|||||||
try:
|
try:
|
||||||
self.client.images.pull(image)
|
self.client.images.pull(image)
|
||||||
except DockerException: # pragma: no cover
|
except DockerException: # pragma: no cover
|
||||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
|
||||||
self.client.images.pull(image)
|
self.client.images.pull(image)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
|||||||
label = "authentik_policies"
|
label = "authentik_policies"
|
||||||
verbose_name = "authentik Policies"
|
verbose_name = "authentik Policies"
|
||||||
default = True
|
default = True
|
||||||
mountpoint = "policy/"
|
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
{% extends 'login/base_full.html' %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script>
|
|
||||||
let redirecting = false;
|
|
||||||
const checkAuth = async () => {
|
|
||||||
if (redirecting) return true;
|
|
||||||
const url = "{{ check_auth_url }}";
|
|
||||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
|
||||||
try {
|
|
||||||
const result = await fetch(url, {
|
|
||||||
method: "HEAD",
|
|
||||||
});
|
|
||||||
if (result.status >= 400) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
console.debug("authentik/policies/buffer: Continuing");
|
|
||||||
redirecting = true;
|
|
||||||
if ("{{ auth_req_method }}" === "post") {
|
|
||||||
document.querySelector("form").submit();
|
|
||||||
} else {
|
|
||||||
window.location.assign("{{ continue_url|escapejs }}");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let timeout = 100;
|
|
||||||
let offset = 20;
|
|
||||||
let attempt = 0;
|
|
||||||
const main = async () => {
|
|
||||||
attempt += 1;
|
|
||||||
await checkAuth();
|
|
||||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
|
||||||
setTimeout(main, timeout);
|
|
||||||
timeout += (offset * attempt);
|
|
||||||
if (timeout >= 2000) {
|
|
||||||
timeout = 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("visibilitychange", async () => {
|
|
||||||
if (document.hidden) return;
|
|
||||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
|
||||||
await checkAuth();
|
|
||||||
});
|
|
||||||
main();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card_title %}
|
|
||||||
{% trans 'Waiting for authentication...' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
|
||||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
|
||||||
{% if auth_req_method == "post" %}
|
|
||||||
{% for key, value in auth_req_body.items %}
|
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="pf-c-empty-state">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<div class="pf-c-empty-state__icon">
|
|
||||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
|
||||||
{% trans "Authenticate in this tab" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -1,121 +0,0 @@
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.test import RequestFactory, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
|
||||||
from authentik.flows.models import FlowDesignation
|
|
||||||
from authentik.flows.planner import FlowPlan
|
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
|
||||||
from authentik.policies.views import (
|
|
||||||
QS_BUFFER_ID,
|
|
||||||
SESSION_KEY_BUFFER,
|
|
||||||
BufferedPolicyAccessView,
|
|
||||||
BufferView,
|
|
||||||
PolicyAccessView,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyViews(TestCase):
|
|
||||||
"""Test PolicyAccessView"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.user = create_test_user()
|
|
||||||
|
|
||||||
def test_pav(self):
|
|
||||||
"""Test simple policy access view"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
|
|
||||||
class TestView(PolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/")
|
|
||||||
req.user = self.user
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertEqual(res.content, b"foo")
|
|
||||||
|
|
||||||
def test_pav_buffer(self):
|
|
||||||
"""Test simple policy access view"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
|
|
||||||
class TestView(BufferedPolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
|
||||||
req.session.save()
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
|
||||||
|
|
||||||
def test_pav_buffer_skip(self):
|
|
||||||
"""Test simple policy access view (skip buffer)"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
|
|
||||||
class TestView(BufferedPolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/?skip_buffer=true")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
|
||||||
req.session.save()
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
|
|
||||||
|
|
||||||
def test_buffer(self):
|
|
||||||
"""Test buffer view"""
|
|
||||||
uid = generate_id()
|
|
||||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
ts = generate_id()
|
|
||||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
|
||||||
"method": "get",
|
|
||||||
"body": {},
|
|
||||||
"url": f"/{ts}",
|
|
||||||
}
|
|
||||||
req.session.save()
|
|
||||||
|
|
||||||
res = BufferView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertIn(ts, res.render().content.decode())
|
|
@ -1,14 +1,7 @@
|
|||||||
"""API URLs"""
|
"""API URLs"""
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||||
from authentik.policies.api.policies import PolicyViewSet
|
from authentik.policies.api.policies import PolicyViewSet
|
||||||
from authentik.policies.views import BufferView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("buffer", BufferView.as_view(), name="buffer"),
|
|
||||||
]
|
|
||||||
|
|
||||||
api_urlpatterns = [
|
api_urlpatterns = [
|
||||||
("policies/all", PolicyViewSet),
|
("policies/all", PolicyViewSet),
|
||||||
|
@ -1,37 +1,23 @@
|
|||||||
"""authentik access helper classes"""
|
"""authentik access helper classes"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import AccessMixin
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.http import urlencode
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.base import TemplateView, View
|
from django.views.generic.base import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider, User
|
from authentik.core.models import Application, Provider, User
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
|
||||||
from authentik.flows.planner import FlowPlan
|
|
||||||
from authentik.flows.views.executor import (
|
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
|
||||||
SESSION_KEY_AUTH_STARTED,
|
|
||||||
SESSION_KEY_PLAN,
|
|
||||||
SESSION_KEY_POST,
|
|
||||||
)
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
QS_BUFFER_ID = "af_bf_id"
|
|
||||||
QS_SKIP_BUFFER = "skip_buffer"
|
|
||||||
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
|
|
||||||
|
|
||||||
|
|
||||||
class RequestValidationError(SentryIgnoredException):
|
class RequestValidationError(SentryIgnoredException):
|
||||||
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
|
|||||||
for message in result.messages:
|
for message in result.messages:
|
||||||
messages.error(self.request, _(message))
|
messages.error(self.request, _(message))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def url_with_qs(url: str, **kwargs):
|
|
||||||
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
|
|
||||||
parameters are retained"""
|
|
||||||
if "?" not in url:
|
|
||||||
return url + f"?{urlencode(kwargs)}"
|
|
||||||
url, _, qs = url.partition("?")
|
|
||||||
qs = QueryDict(qs, mutable=True)
|
|
||||||
qs.update(kwargs)
|
|
||||||
return url + f"?{urlencode(qs.items())}"
|
|
||||||
|
|
||||||
|
|
||||||
class BufferView(TemplateView):
|
|
||||||
"""Buffer view"""
|
|
||||||
|
|
||||||
template_name = "policies/buffer.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
buf_id = self.request.GET.get(QS_BUFFER_ID)
|
|
||||||
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
|
|
||||||
kwargs["auth_req_method"] = buffer["method"]
|
|
||||||
kwargs["auth_req_body"] = buffer["body"]
|
|
||||||
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
|
|
||||||
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
|
|
||||||
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BufferedPolicyAccessView(PolicyAccessView):
|
|
||||||
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
|
|
||||||
|
|
||||||
def handle_no_permission(self):
|
|
||||||
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
|
|
||||||
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
|
|
||||||
if plan:
|
|
||||||
flow = Flow.objects.filter(pk=plan.flow_pk).first()
|
|
||||||
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
|
|
||||||
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
if not plan and authenticating is None:
|
|
||||||
LOGGER.debug("Not buffering request, no flow plan active")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
if self.request.GET.get(QS_SKIP_BUFFER):
|
|
||||||
LOGGER.debug("Not buffering request, explicit skip")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
buffer_id = str(uuid4())
|
|
||||||
LOGGER.debug("Buffering access request", bf_id=buffer_id)
|
|
||||||
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
|
|
||||||
"body": self.request.POST,
|
|
||||||
"url": self.request.build_absolute_uri(self.request.get_full_path()),
|
|
||||||
"method": self.request.method.lower(),
|
|
||||||
}
|
|
||||||
return redirect(
|
|
||||||
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
|
||||||
if QS_BUFFER_ID in self.request.GET:
|
|
||||||
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
|
|
||||||
return response
|
|
||||||
|
@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
|
|||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
|
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
PKCE_METHOD_PLAIN,
|
PKCE_METHOD_PLAIN,
|
||||||
PKCE_METHOD_S256,
|
PKCE_METHOD_S256,
|
||||||
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
|
|||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
||||||
|
|
||||||
params: OAuthAuthorizationParams
|
params: OAuthAuthorizationParams
|
||||||
|
@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
|||||||
from authentik.flows.stage import RedirectStage
|
from authentik.flows.stage import RedirectStage
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.views import BufferedPolicyAccessView
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||||
|
|
||||||
|
|
||||||
class RACStartView(BufferedPolicyAccessView):
|
class RACStartView(PolicyAccessView):
|
||||||
"""Start a RAC connection by checking access and creating a connection token"""
|
"""Start a RAC connection by checking access and creating a connection token"""
|
||||||
|
|
||||||
endpoint: Endpoint
|
endpoint: Endpoint
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-31 13:50
|
|
||||||
|
|
||||||
import authentik.lib.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlprovider",
|
|
||||||
name="acs_url",
|
|
||||||
field=models.TextField(
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="ACS URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
|
|||||||
from authentik.core.api.object_types import CreatableType
|
from authentik.core.api.object_types import CreatableType
|
||||||
from authentik.core.models import PropertyMapping, Provider
|
from authentik.core.models import PropertyMapping, Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
|
|||||||
class SAMLProvider(Provider):
|
class SAMLProvider(Provider):
|
||||||
"""SAML 2.0 Endpoint for applications which support SAML."""
|
"""SAML 2.0 Endpoint for applications which support SAML."""
|
||||||
|
|
||||||
acs_url = models.TextField(
|
acs_url = models.URLField(verbose_name=_("ACS URL"))
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
|
|
||||||
)
|
|
||||||
audience = models.TextField(
|
audience = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.views.executor import SESSION_KEY_POST
|
from authentik.flows.views.executor import SESSION_KEY_POST
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.views import BufferedPolicyAccessView
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||||
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class SAMLSSOView(BufferedPolicyAccessView):
|
class SAMLSSOView(PolicyAccessView):
|
||||||
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
||||||
Calls get/post handler."""
|
Calls get/post handler."""
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
|
|||||||
|
|
||||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
"""GET and POST use the same handler, but we can't
|
"""GET and POST use the same handler, but we can't
|
||||||
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
|
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||||
return self.get(request, application_slug)
|
return self.get(request, application_slug)
|
||||||
|
|
||||||
|
|
||||||
|
@ -243,10 +243,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
|||||||
if user.value not in users_should:
|
if user.value not in users_should:
|
||||||
users_to_remove.append(user.value)
|
users_to_remove.append(user.value)
|
||||||
# Check users that should be in the group and add them
|
# Check users that should be in the group and add them
|
||||||
if current_group.members is not None:
|
for user in users_should:
|
||||||
for user in users_should:
|
if len([x for x in current_group.members if x.value == user]) < 1:
|
||||||
if len([x for x in current_group.members if x.value == user]) < 1:
|
users_to_add.append(user)
|
||||||
users_to_add.append(user)
|
|
||||||
# Only send request if we need to make changes
|
# Only send request if we need to make changes
|
||||||
if len(users_to_add) < 1 and len(users_to_remove) < 1:
|
if len(users_to_add) < 1 and len(users_to_remove) < 1:
|
||||||
return
|
return
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-31 13:53
|
|
||||||
|
|
||||||
import authentik.lib.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_sources_saml", "0017_fix_x509subjectname"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlsource",
|
|
||||||
name="slo_url",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
help_text="Optional URL if your IDP supports Single-Logout.",
|
|
||||||
null=True,
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="SLO URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlsource",
|
|
||||||
name="sso_url",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="URL that the initial Login request is sent to.",
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="SSO URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.flows.challenge import RedirectChallenge
|
from authentik.flows.challenge import RedirectChallenge
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
@ -92,13 +91,11 @@ class SAMLSource(Source):
|
|||||||
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
||||||
)
|
)
|
||||||
|
|
||||||
sso_url = models.TextField(
|
sso_url = models.URLField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name=_("SSO URL"),
|
verbose_name=_("SSO URL"),
|
||||||
help_text=_("URL that the initial Login request is sent to."),
|
help_text=_("URL that the initial Login request is sent to."),
|
||||||
)
|
)
|
||||||
slo_url = models.TextField(
|
slo_url = models.URLField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -33,7 +33,6 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import is_url_absolute
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.providers.saml.utils.encoding import nice64
|
from authentik.providers.saml.utils.encoding import nice64
|
||||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||||
@ -74,8 +73,6 @@ class InitiateView(View):
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
if not is_url_absolute(final_redirect):
|
|
||||||
final_redirect = "authentik_core:if-user"
|
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
PLAN_CONTEXT_SSO: True,
|
PLAN_CONTEXT_SSO: True,
|
||||||
|
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
@ -67,36 +67,6 @@ class TestEmailStageSending(FlowTestCase):
|
|||||||
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
||||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||||
|
|
||||||
def test_newlines_long_name(self):
|
|
||||||
"""Test with pending user"""
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
|
||||||
long_user = create_test_user()
|
|
||||||
long_user.name = "Test User\r\n Many Words\r\n"
|
|
||||||
long_user.save()
|
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
|
|
||||||
session = self.client.session
|
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session.save()
|
|
||||||
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
|
||||||
|
|
||||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
|
||||||
with patch(
|
|
||||||
"authentik.stages.email.models.EmailStage.backend_class",
|
|
||||||
PropertyMock(return_value=EmailBackend),
|
|
||||||
):
|
|
||||||
response = self.client.post(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertStageResponse(
|
|
||||||
response,
|
|
||||||
self.flow,
|
|
||||||
response_errors={
|
|
||||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
|
||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
|
||||||
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
|
||||||
|
|
||||||
def test_pending_fake_user(self):
|
def test_pending_fake_user(self):
|
||||||
"""Test with pending (fake) user"""
|
"""Test with pending (fake) user"""
|
||||||
self.flow.designation = FlowDesignation.RECOVERY
|
self.flow.designation = FlowDesignation.RECOVERY
|
||||||
|
@ -32,14 +32,7 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
|||||||
sanitized_to = []
|
sanitized_to = []
|
||||||
# Ensure that all recipients are valid
|
# Ensure that all recipients are valid
|
||||||
for recipient_name, recipient_email in to:
|
for recipient_name, recipient_email in to:
|
||||||
# Remove any newline characters from name and email before sanitizing
|
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
||||||
clean_name = (
|
|
||||||
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
|
|
||||||
)
|
|
||||||
clean_email = (
|
|
||||||
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
|
|
||||||
)
|
|
||||||
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
|
|
||||||
super().__init__(to=sanitized_to, **kwargs)
|
super().__init__(to=sanitized_to, **kwargs)
|
||||||
if not template_name:
|
if not template_name:
|
||||||
return
|
return
|
||||||
|
@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
|||||||
raise ValidationError("Failed to authenticate.")
|
raise ValidationError("Failed to authenticate.")
|
||||||
self.pre_user = pre_user
|
self.pre_user = pre_user
|
||||||
|
|
||||||
|
# Password check
|
||||||
|
if current_stage.password_stage:
|
||||||
|
password = attrs.get("password", None)
|
||||||
|
if not password:
|
||||||
|
self.stage.logger.warning("Password not set for ident+auth attempt")
|
||||||
|
try:
|
||||||
|
with start_span(
|
||||||
|
op="authentik.stages.identification.authenticate",
|
||||||
|
name="User authenticate call (combo stage)",
|
||||||
|
):
|
||||||
|
user = authenticate(
|
||||||
|
self.stage.request,
|
||||||
|
current_stage.password_stage.backends,
|
||||||
|
current_stage,
|
||||||
|
username=self.pre_user.username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise ValidationError("Failed to authenticate.")
|
||||||
|
self.pre_user = user
|
||||||
|
except PermissionDenied as exc:
|
||||||
|
raise ValidationError(str(exc)) from exc
|
||||||
|
|
||||||
# Captcha check
|
# Captcha check
|
||||||
if captcha_stage := current_stage.captcha_stage:
|
if captcha_stage := current_stage.captcha_stage:
|
||||||
captcha_token = attrs.get("captcha_token", None)
|
captcha_token = attrs.get("captcha_token", None)
|
||||||
if not captcha_token:
|
if not captcha_token:
|
||||||
self.stage.logger.warning("Token not set for captcha attempt")
|
self.stage.logger.warning("Token not set for captcha attempt")
|
||||||
verify_captcha_token(captcha_stage, captcha_token, client_ip)
|
verify_captcha_token(captcha_stage, captcha_token, client_ip)
|
||||||
|
|
||||||
# Password check
|
|
||||||
if not current_stage.password_stage:
|
|
||||||
# No password stage select, don't validate the password
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
password = attrs.get("password", None)
|
|
||||||
if not password:
|
|
||||||
self.stage.logger.warning("Password not set for ident+auth attempt")
|
|
||||||
try:
|
|
||||||
with start_span(
|
|
||||||
op="authentik.stages.identification.authenticate",
|
|
||||||
name="User authenticate call (combo stage)",
|
|
||||||
):
|
|
||||||
user = authenticate(
|
|
||||||
self.stage.request,
|
|
||||||
current_stage.password_stage.backends,
|
|
||||||
current_stage,
|
|
||||||
username=self.pre_user.username,
|
|
||||||
password=password,
|
|
||||||
)
|
|
||||||
if not user:
|
|
||||||
raise ValidationError("Failed to authenticate.")
|
|
||||||
self.pre_user = user
|
|
||||||
except PermissionDenied as exc:
|
|
||||||
raise ValidationError(str(exc)) from exc
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2025.2.3 Blueprint schema",
|
"title": "authentik 2025.2.2 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
@ -6423,6 +6423,8 @@
|
|||||||
},
|
},
|
||||||
"acs_url": {
|
"acs_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "ACS URL"
|
"title": "ACS URL"
|
||||||
},
|
},
|
||||||
@ -8731,6 +8733,8 @@
|
|||||||
},
|
},
|
||||||
"sso_url": {
|
"sso_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "SSO URL",
|
"title": "SSO URL",
|
||||||
"description": "URL that the initial Login request is sent to."
|
"description": "URL that the initial Login request is sent to."
|
||||||
@ -8740,6 +8744,8 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"title": "SLO URL",
|
"title": "SLO URL",
|
||||||
"description": "Optional URL if your IDP supports Single-Logout."
|
"description": "Optional URL if your IDP supports Single-Logout."
|
||||||
},
|
},
|
||||||
@ -13010,15 +13016,6 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Branding favicon"
|
"title": "Branding favicon"
|
||||||
},
|
},
|
||||||
"branding_custom_css": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Branding custom css"
|
|
||||||
},
|
|
||||||
"branding_default_flow_background": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Branding default flow background"
|
|
||||||
},
|
|
||||||
"flow_authentication": {
|
"flow_authentication": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
@ -14900,15 +14897,9 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Webhook url"
|
"title": "Webhook url"
|
||||||
},
|
},
|
||||||
"webhook_mapping_body": {
|
"webhook_mapping": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"title": "Webhook mapping body",
|
"title": "Webhook mapping"
|
||||||
"description": "Customize the body of the request. Mapping should return data that is JSON-serializable."
|
|
||||||
},
|
|
||||||
"webhook_mapping_headers": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Webhook mapping headers",
|
|
||||||
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
|
|
||||||
},
|
},
|
||||||
"send_once": {
|
"send_once": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -54,7 +54,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
13
go.mod
13
go.mod
@ -1,6 +1,9 @@
|
|||||||
module goauthentik.io
|
module goauthentik.io
|
||||||
|
|
||||||
go 1.24.0
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
beryju.io/ldap v0.1.0
|
beryju.io/ldap v0.1.0
|
||||||
github.com/coreos/go-oidc/v3 v3.13.0
|
github.com/coreos/go-oidc/v3 v3.13.0
|
||||||
@ -8,7 +11,7 @@ require (
|
|||||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.10
|
||||||
github.com/go-openapi/runtime v0.28.0
|
github.com/go-openapi/runtime v0.28.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/handlers v1.5.2
|
github.com/gorilla/handlers v1.5.2
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
@ -20,13 +23,13 @@ require (
|
|||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||||
github.com/pires/go-proxyproto v0.8.0
|
github.com/pires/go-proxyproto v0.8.0
|
||||||
github.com/prometheus/client_golang v1.21.1
|
github.com/prometheus/client_golang v1.21.1
|
||||||
github.com/redis/go-redis/v9 v9.7.3
|
github.com/redis/go-redis/v9 v9.7.1
|
||||||
github.com/sethvargo/go-envconfig v1.1.1
|
github.com/sethvargo/go-envconfig v1.1.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2025023.2
|
goauthentik.io/api/v3 v3.2025022.3
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.12.0
|
golang.org/x/sync v0.12.0
|
||||||
@ -79,3 +82,5 @@ require (
|
|||||||
google.golang.org/protobuf v1.36.1 // indirect
|
google.golang.org/protobuf v1.36.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace goauthentik.io/api/v3 => ./gen-go-api
|
||||||
|
12
go.sum
12
go.sum
@ -113,8 +113,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
|
|||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -248,8 +248,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
|
||||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
goauthentik.io/api/v3 v3.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA=
|
goauthentik.io/api/v3 v3.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
|
||||||
goauthentik.io/api/v3 v3.2025023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -162,14 +162,13 @@ func (c *Config) parseScheme(rawVal string) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return rawVal
|
return rawVal
|
||||||
}
|
}
|
||||||
switch u.Scheme {
|
if u.Scheme == "env" {
|
||||||
case "env":
|
|
||||||
e, ok := os.LookupEnv(u.Host)
|
e, ok := os.LookupEnv(u.Host)
|
||||||
if ok {
|
if ok {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
return u.RawQuery
|
return u.RawQuery
|
||||||
case "file":
|
} else if u.Scheme == "file" {
|
||||||
d, err := os.ReadFile(u.Path)
|
d, err := os.ReadFile(u.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u.RawQuery
|
return u.RawQuery
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigEnv(t *testing.T) {
|
func TestConfigEnv(t *testing.T) {
|
||||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar"))
|
os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
|
||||||
cfg = nil
|
cfg = nil
|
||||||
if err := Get().fromEnv(); err != nil {
|
if err := Get().fromEnv(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigEnv_Scheme(t *testing.T) {
|
func TestConfigEnv_Scheme(t *testing.T) {
|
||||||
assert.NoError(t, os.Setenv("foo", "bar"))
|
os.Setenv("foo", "bar")
|
||||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo"))
|
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
|
||||||
cfg = nil
|
cfg = nil
|
||||||
if err := Get().fromEnv(); err != nil {
|
if err := Get().fromEnv(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -33,15 +33,13 @@ func TestConfigEnv_File(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer os.Remove(file.Name())
|
||||||
assert.NoError(t, os.Remove(file.Name()))
|
|
||||||
}()
|
|
||||||
_, err = file.Write([]byte("bar"))
|
_, err = file.Write([]byte("bar"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())))
|
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
|
||||||
cfg = nil
|
cfg = nil
|
||||||
if err := Get().fromEnv(); err != nil {
|
if err := Get().fromEnv(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2025.2.3"
|
const VERSION = "2025.2.2"
|
||||||
|
5
internal/crypto/backend/fips_disabled.go
Normal file
5
internal/crypto/backend/fips_disabled.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build requirefips
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
var FipsEnabled = true
|
5
internal/crypto/backend/fips_enabled.go
Normal file
5
internal/crypto/backend/fips_enabled.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build !requirefips
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
var FipsEnabled = false
|
@ -35,7 +35,7 @@ func EnableDebugServer() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl)
|
_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("failed to write index")
|
l.WithError(err).Warning("failed to write index")
|
||||||
return nil
|
return nil
|
||||||
|
@ -44,11 +44,10 @@ func New(healthcheck func() bool) *GoUnicorn {
|
|||||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
|
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
|
||||||
go func() {
|
go func() {
|
||||||
for sig := range c {
|
for sig := range c {
|
||||||
switch sig {
|
if sig == syscall.SIGHUP {
|
||||||
case syscall.SIGHUP:
|
|
||||||
g.log.Info("SIGHUP received, forwarding to gunicorn")
|
g.log.Info("SIGHUP received, forwarding to gunicorn")
|
||||||
g.Reload()
|
g.Reload()
|
||||||
case syscall.SIGUSR2:
|
} else if sig == syscall.SIGUSR2 {
|
||||||
g.log.Info("SIGUSR2 received, restarting gunicorn")
|
g.log.Info("SIGUSR2 received, restarting gunicorn")
|
||||||
g.Restart()
|
g.Restart()
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package ak
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/fips140"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
|
|||||||
"golangVersion": runtime.Version(),
|
"golangVersion": runtime.Version(),
|
||||||
"opensslEnabled": cryptobackend.OpensslEnabled,
|
"opensslEnabled": cryptobackend.OpensslEnabled,
|
||||||
"opensslVersion": cryptobackend.OpensslVersion(),
|
"opensslVersion": cryptobackend.OpensslVersion(),
|
||||||
"fipsEnabled": fips140.Enabled(),
|
"fipsEnabled": cryptobackend.FipsEnabled,
|
||||||
}
|
}
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -35,19 +35,13 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
|||||||
req PaginatorRequest[Treq, Tres],
|
req PaginatorRequest[Treq, Tres],
|
||||||
opts PaginatorOptions,
|
opts PaginatorOptions,
|
||||||
) ([]Tobj, error) {
|
) ([]Tobj, error) {
|
||||||
if opts.Logger == nil {
|
|
||||||
opts.Logger = log.NewEntry(log.StandardLogger())
|
|
||||||
}
|
|
||||||
var bfreq, cfreq interface{}
|
var bfreq, cfreq interface{}
|
||||||
fetchOffset := func(page int32) (Tres, error) {
|
fetchOffset := func(page int32) (Tres, error) {
|
||||||
bfreq = req.Page(page)
|
bfreq = req.Page(page)
|
||||||
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
||||||
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
||||||
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@ -57,9 +51,6 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
|||||||
for {
|
for {
|
||||||
apiObjects, err := fetchOffset(page)
|
apiObjects, err := fetchOffset(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if page == 1 {
|
|
||||||
return objects, err
|
|
||||||
}
|
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,5 @@
|
|||||||
package ak
|
package ak
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"goauthentik.io/api/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeAPIType struct{}
|
|
||||||
|
|
||||||
type fakeAPIResponse struct {
|
|
||||||
results []fakeAPIType
|
|
||||||
pagination api.Pagination
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
|
|
||||||
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
|
|
||||||
|
|
||||||
type fakeAPIRequest struct {
|
|
||||||
res *fakeAPIResponse
|
|
||||||
http *http.Response
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
|
|
||||||
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
|
|
||||||
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
|
|
||||||
return fapi.res, fapi.http, fapi.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Simple(t *testing.T) {
|
|
||||||
req := &fakeAPIRequest{
|
|
||||||
res: &fakeAPIResponse{
|
|
||||||
results: []fakeAPIType{
|
|
||||||
{},
|
|
||||||
},
|
|
||||||
pagination: api.Pagination{
|
|
||||||
TotalPages: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res, err := Paginator(req, PaginatorOptions{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, res, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_BadRequest(t *testing.T) {
|
|
||||||
req := &fakeAPIRequest{
|
|
||||||
http: &http.Response{
|
|
||||||
StatusCode: 400,
|
|
||||||
},
|
|
||||||
err: errors.New("foo"),
|
|
||||||
}
|
|
||||||
res, err := Paginator(req, PaginatorOptions{})
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, []fakeAPIType{}, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func Test_PaginatorCompile(t *testing.T) {
|
// func Test_PaginatorCompile(t *testing.T) {
|
||||||
// req := api.ApiCoreUsersListRequest{}
|
// req := api.ApiCoreUsersListRequest{}
|
||||||
// Paginator(req, PaginatorOptions{
|
// Paginator(req, PaginatorOptions{
|
||||||
|
@ -148,8 +148,7 @@ func (ac *APIController) startWSHandler() {
|
|||||||
"outpost_type": ac.Server.Type(),
|
"outpost_type": ac.Server.Type(),
|
||||||
"uuid": ac.instanceUUID.String(),
|
"uuid": ac.instanceUUID.String(),
|
||||||
}).Set(1)
|
}).Set(1)
|
||||||
switch wsMsg.Instruction {
|
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
|
||||||
case WebsocketInstructionTriggerUpdate:
|
|
||||||
time.Sleep(ac.reloadOffset)
|
time.Sleep(ac.reloadOffset)
|
||||||
logger.Debug("Got update trigger...")
|
logger.Debug("Got update trigger...")
|
||||||
err := ac.OnRefresh()
|
err := ac.OnRefresh()
|
||||||
@ -164,7 +163,7 @@ func (ac *APIController) startWSHandler() {
|
|||||||
"build": constants.BUILD(""),
|
"build": constants.BUILD(""),
|
||||||
}).SetToCurrentTime()
|
}).SetToCurrentTime()
|
||||||
}
|
}
|
||||||
case WebsocketInstructionProviderSpecific:
|
} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
|
||||||
for _, h := range ac.wsHandlers {
|
for _, h := range ac.wsHandlers {
|
||||||
h(context.Background(), wsMsg.Args)
|
h(context.Background(), wsMsg.Args)
|
||||||
}
|
}
|
||||||
|
@ -66,12 +66,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
||||||
err = ls.s.Serve(proxyListener)
|
err = ls.s.Serve(proxyListener)
|
||||||
|
@ -49,12 +49,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
|||||||
|
|
||||||
entries := make([]*ldap.Entry, 0)
|
entries := make([]*ldap.Entry, 0)
|
||||||
|
|
||||||
scope := req.Scope
|
scope := req.SearchRequest.Scope
|
||||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||||
|
|
||||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||||
|
@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
|
|||||||
if !embedded && hostBrowser == "" {
|
if !embedded && hostBrowser == "" {
|
||||||
return ep
|
return ep
|
||||||
}
|
}
|
||||||
var newHost = aku
|
var newHost *url.URL = aku
|
||||||
var newBrowserHost *url.URL
|
var newBrowserHost *url.URL
|
||||||
if embedded {
|
if embedded {
|
||||||
if authentikHost == "" {
|
if authentikHost == "" {
|
||||||
|
@ -130,12 +130,7 @@ func (ps *ProxyServer) ServeHTTP() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
||||||
ps.serve(proxyListener)
|
ps.serve(proxyListener)
|
||||||
@ -154,12 +149,7 @@ func (ps *ProxyServer) ServeHTTPS() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")
|
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")
|
||||||
|
@ -72,13 +72,11 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
|
|||||||
session.ID = c.Value
|
session.ID = c.Value
|
||||||
|
|
||||||
err = s.load(r.Context(), session)
|
err = s.load(r.Context(), session)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
if errors.Is(err, redis.Nil) {
|
session.IsNew = false
|
||||||
return session, nil
|
} else if err == redis.Nil {
|
||||||
}
|
err = nil // no data stored
|
||||||
return session, err
|
|
||||||
}
|
}
|
||||||
session.IsNew = false
|
|
||||||
return session, err
|
return session, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
|
<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/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/authentik.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
|
||||||
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
|
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
|
||||||
<style>
|
<style>
|
||||||
.pf-c-background-image::before {
|
.pf-c-background-image::before {
|
||||||
|
@ -156,12 +156,7 @@ func (ws *WebServer) listenPlain() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
||||||
ws.serve(proxyListener)
|
ws.serve(proxyListener)
|
||||||
|
@ -46,12 +46,7 @@ func (ws *WebServer) listenTLS() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer func() {
|
defer proxyListener.Close()
|
||||||
err := proxyListener.Close()
|
|
||||||
if err != nil {
|
|
||||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||||
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")
|
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/ldap ./cmd/ldap
|
go build -o /go/ldap ./cmd/ldap
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1006.0",
|
"aws-cdk": "^2.1005.0",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aws-cdk": {
|
"node_modules/aws-cdk": {
|
||||||
"version": "2.1006.0",
|
"version": "2.1005.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
|
||||||
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
|
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1006.0",
|
"aws-cdk": "^2.1005.0",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ Parameters:
|
|||||||
Description: authentik Docker image
|
Description: authentik Docker image
|
||||||
AuthentikVersion:
|
AuthentikVersion:
|
||||||
Type: String
|
Type: String
|
||||||
Default: 2025.2.3
|
Default: 2025.2.2
|
||||||
Description: authentik Docker image tag
|
Description: authentik Docker image tag
|
||||||
AuthentikServerCPU:
|
AuthentikServerCPU:
|
||||||
Type: Number
|
Type: Number
|
||||||
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -616,18 +616,6 @@ msgstr ""
|
|||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
|
||||||
"serializable."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
|
||||||
"of key-value pairs"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only send notification once, for example when sending a webhook into a chat "
|
"Only send notification once, for example when sending a webhook into a chat "
|
||||||
@ -1220,20 +1208,6 @@ msgstr ""
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1782,17 +1756,6 @@ msgid ""
|
|||||||
"NameIDPolicy of the incoming request will be considered"
|
"NameIDPolicy of the incoming request will be considered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid "AuthnContextClassRef Property Mapping"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure how the AuthnContextClassRef value will be created. When left "
|
|
||||||
"empty, the AuthnContextClassRef will be set based on which authentication "
|
|
||||||
"methods the user used to authenticate."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assertion valid not before current time + this value (Format: hours=-1;"
|
"Assertion valid not before current time + this value (Format: hours=-1;"
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
# Manuel Viens, 2023
|
# Manuel Viens, 2023
|
||||||
# Mordecai, 2023
|
# Mordecai, 2023
|
||||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||||
|
# Tina, 2024
|
||||||
# Charles Leclerc, 2025
|
# Charles Leclerc, 2025
|
||||||
# Tina, 2025
|
|
||||||
# Marc Schmitt, 2025
|
# Marc Schmitt, 2025
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -19,7 +19,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: Marc Schmitt, 2025\n"
|
"Last-Translator: Marc Schmitt, 2025\n"
|
||||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||||
@ -676,22 +676,6 @@ msgstr "Webhook Slack (ou Discord)"
|
|||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Courriel"
|
msgstr "Courriel"
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
|
||||||
"serializable."
|
|
||||||
msgstr ""
|
|
||||||
"Personnalise le corps de la requête. Le mappage doit renvoyer des données "
|
|
||||||
"sérialisables en JSON."
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
|
||||||
"of key-value pairs"
|
|
||||||
msgstr ""
|
|
||||||
"Configure les en-têtes supplémentaires à envoyer. Le mappage doit renvoyer "
|
|
||||||
"un dictionnaire de paires clé-valeur."
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only send notification once, for example when sending a webhook into a chat "
|
"Only send notification once, for example when sending a webhook into a chat "
|
||||||
@ -1347,22 +1331,6 @@ msgstr "Score de Réputation"
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr "Scores de Réputation"
|
msgstr "Scores de Réputation"
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr "En attente de l'authentification..."
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr ""
|
|
||||||
"Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page "
|
|
||||||
"se rafraîchira lorsque l'authentification sera terminée."
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr "S'authentifier dans cet onglet"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr "Permission refusée"
|
msgstr "Permission refusée"
|
||||||
@ -1988,20 +1956,6 @@ msgstr ""
|
|||||||
"Configure la manière dont la valeur NameID sera créée. Si laissé vide, la "
|
"Configure la manière dont la valeur NameID sera créée. Si laissé vide, la "
|
||||||
"NameIDPolicy de la requête entrante sera prise en compte"
|
"NameIDPolicy de la requête entrante sera prise en compte"
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid "AuthnContextClassRef Property Mapping"
|
|
||||||
msgstr "Mappage de propriété AuthnContextClassRef"
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure how the AuthnContextClassRef value will be created. When left "
|
|
||||||
"empty, the AuthnContextClassRef will be set based on which authentication "
|
|
||||||
"methods the user used to authenticate."
|
|
||||||
msgstr ""
|
|
||||||
"Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non "
|
|
||||||
"sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode "
|
|
||||||
"d'authentification l'utilisateur a utilisé pour s'authentifier."
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assertion valid not before current time + this value (Format: "
|
"Assertion valid not before current time + this value (Format: "
|
||||||
|
Binary file not shown.
@ -15,7 +15,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2025\n"
|
"Last-Translator: deluxghost, 2025\n"
|
||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||||
@ -627,18 +627,6 @@ msgstr "Slack Webhook(Slack/Discord)"
|
|||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "电子邮箱"
|
msgstr "电子邮箱"
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
|
||||||
"serializable."
|
|
||||||
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
|
||||||
"of key-value pairs"
|
|
||||||
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only send notification once, for example when sending a webhook into a chat "
|
"Only send notification once, for example when sending a webhook into a chat "
|
||||||
@ -1794,18 +1782,6 @@ msgid ""
|
|||||||
"NameIDPolicy of the incoming request will be considered"
|
"NameIDPolicy of the incoming request will be considered"
|
||||||
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid "AuthnContextClassRef Property Mapping"
|
|
||||||
msgstr "AuthnContextClassRef 属性映射"
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure how the AuthnContextClassRef value will be created. When left "
|
|
||||||
"empty, the AuthnContextClassRef will be set based on which authentication "
|
|
||||||
"methods the user used to authenticate."
|
|
||||||
msgstr ""
|
|
||||||
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assertion valid not before current time + this value (Format: "
|
"Assertion valid not before current time + this value (Format: "
|
||||||
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2025\n"
|
"Last-Translator: deluxghost, 2025\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -626,18 +626,6 @@ msgstr "Slack Webhook(Slack/Discord)"
|
|||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "电子邮箱"
|
msgstr "电子邮箱"
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
|
||||||
"serializable."
|
|
||||||
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
|
||||||
"of key-value pairs"
|
|
||||||
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
|
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only send notification once, for example when sending a webhook into a chat "
|
"Only send notification once, for example when sending a webhook into a chat "
|
||||||
@ -1234,20 +1222,6 @@ msgstr "信誉分数"
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr "信誉分数"
|
msgstr "信誉分数"
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr "正在等待身份验证…"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr "在此标签页中验证身份"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr "权限被拒绝"
|
msgstr "权限被拒绝"
|
||||||
@ -1807,18 +1781,6 @@ msgid ""
|
|||||||
"NameIDPolicy of the incoming request will be considered"
|
"NameIDPolicy of the incoming request will be considered"
|
||||||
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid "AuthnContextClassRef Property Mapping"
|
|
||||||
msgstr "AuthnContextClassRef 属性映射"
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
|
||||||
msgid ""
|
|
||||||
"Configure how the AuthnContextClassRef value will be created. When left "
|
|
||||||
"empty, the AuthnContextClassRef will be set based on which authentication "
|
|
||||||
"methods the user used to authenticate."
|
|
||||||
msgstr ""
|
|
||||||
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
|
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assertion valid not before current time + this value (Format: "
|
"Assertion valid not before current time + this value (Format: "
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2025.2.3",
|
"version": "2025.2.2",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ COPY web .
|
|||||||
RUN npm run build-proxy
|
RUN npm run build-proxy
|
||||||
|
|
||||||
# Stage 2: Build
|
# Stage 2: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -43,7 +43,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/proxy ./cmd/proxy
|
go build -o /go/proxy ./cmd/proxy
|
||||||
|
|
||||||
# Stage 3: Run
|
# Stage 3: Run
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2025.2.3"
|
version = "2025.2.2"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||||
requires-python = "==3.12.*"
|
requires-python = "==3.12.*"
|
||||||
@ -103,7 +103,7 @@ dev = [
|
|||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
||||||
opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8" }
|
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ak = "lifecycle.ak:main"
|
ak = "lifecycle.ak:main"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/rac ./cmd/rac
|
go build -o /go/rac ./cmd/rac
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/radius ./cmd/radius
|
go build -o /go/radius ./cmd/radius
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
170
schema.yml
170
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2025.2.3
|
version: 2025.2.2
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -4447,10 +4447,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
- in: query
|
|
||||||
name: branding_default_flow_background
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
- in: query
|
||||||
name: branding_favicon
|
name: branding_favicon
|
||||||
schema:
|
schema:
|
||||||
@ -8921,6 +8917,11 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: Querystring as received
|
description: Querystring as received
|
||||||
required: true
|
required: true
|
||||||
|
- in: query
|
||||||
|
name: xid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Flow execution ID
|
||||||
tags:
|
tags:
|
||||||
- flows
|
- flows
|
||||||
security:
|
security:
|
||||||
@ -8961,6 +8962,12 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: Querystring as received
|
description: Querystring as received
|
||||||
required: true
|
required: true
|
||||||
|
- in: query
|
||||||
|
name: xid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Flow execution ID
|
||||||
|
required: true
|
||||||
tags:
|
tags:
|
||||||
- flows
|
- flows
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -39441,6 +39448,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-access-denied
|
default: ak-stage-access-denied
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -39456,6 +39465,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AlgEnum:
|
AlgEnum:
|
||||||
enum:
|
enum:
|
||||||
- rsa
|
- rsa
|
||||||
@ -39555,6 +39565,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-source-oauth-apple
|
default: ak-source-oauth-apple
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -39574,6 +39586,7 @@ components:
|
|||||||
- redirect_uri
|
- redirect_uri
|
||||||
- scope
|
- scope
|
||||||
- state
|
- state
|
||||||
|
- xid
|
||||||
Application:
|
Application:
|
||||||
type: object
|
type: object
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
@ -39882,6 +39895,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-duo
|
default: ak-stage-authenticator-duo
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -39904,6 +39919,7 @@ components:
|
|||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
- stage_uuid
|
- stage_uuid
|
||||||
|
- xid
|
||||||
AuthenticatorDuoChallengeResponseRequest:
|
AuthenticatorDuoChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Pseudo class for duo response
|
description: Pseudo class for duo response
|
||||||
@ -40041,6 +40057,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-email
|
default: ak-stage-authenticator-email
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40060,6 +40078,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AuthenticatorEmailChallengeResponseRequest:
|
AuthenticatorEmailChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Authenticator Email Challenge response, device is set by get_response_instance
|
description: Authenticator Email Challenge response, device is set by get_response_instance
|
||||||
@ -40297,6 +40316,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-sms
|
default: ak-stage-authenticator-sms
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40313,6 +40334,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AuthenticatorSMSChallengeResponseRequest:
|
AuthenticatorSMSChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: SMS Challenge response, device is set by get_response_instance
|
description: SMS Challenge response, device is set by get_response_instance
|
||||||
@ -40460,6 +40482,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-static
|
default: ak-stage-authenticator-static
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40478,6 +40502,7 @@ components:
|
|||||||
- codes
|
- codes
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AuthenticatorStaticChallengeResponseRequest:
|
AuthenticatorStaticChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Pseudo class for static response
|
description: Pseudo class for static response
|
||||||
@ -40581,6 +40606,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-totp
|
default: ak-stage-authenticator-totp
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40597,6 +40624,7 @@ components:
|
|||||||
- config_url
|
- config_url
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AuthenticatorTOTPChallengeResponseRequest:
|
AuthenticatorTOTPChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: TOTP Challenge response, device is set by get_response_instance
|
description: TOTP Challenge response, device is set by get_response_instance
|
||||||
@ -40808,6 +40836,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-validate
|
default: ak-stage-authenticator-validate
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40831,6 +40861,7 @@ components:
|
|||||||
- device_challenges
|
- device_challenges
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
AuthenticatorValidationChallengeResponseRequest:
|
AuthenticatorValidationChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Challenge used for Code-based and WebAuthn authenticators
|
description: Challenge used for Code-based and WebAuthn authenticators
|
||||||
@ -40861,6 +40892,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-authenticator-webauthn
|
default: ak-stage-authenticator-webauthn
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40878,6 +40911,7 @@ components:
|
|||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
- registration
|
- registration
|
||||||
|
- xid
|
||||||
AuthenticatorWebAuthnChallengeResponseRequest:
|
AuthenticatorWebAuthnChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: WebAuthn Challenge response
|
description: WebAuthn Challenge response
|
||||||
@ -41010,6 +41044,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-autosubmit
|
default: ak-stage-autosubmit
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -41027,6 +41063,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- attrs
|
- attrs
|
||||||
- url
|
- url
|
||||||
|
- xid
|
||||||
BackendsEnum:
|
BackendsEnum:
|
||||||
enum:
|
enum:
|
||||||
- authentik.core.auth.InbuiltBackend
|
- authentik.core.auth.InbuiltBackend
|
||||||
@ -41149,10 +41186,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
branding_favicon:
|
branding_favicon:
|
||||||
type: string
|
type: string
|
||||||
branding_custom_css:
|
|
||||||
type: string
|
|
||||||
branding_default_flow_background:
|
|
||||||
type: string
|
|
||||||
flow_authentication:
|
flow_authentication:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
@ -41212,11 +41245,6 @@ components:
|
|||||||
branding_favicon:
|
branding_favicon:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
branding_custom_css:
|
|
||||||
type: string
|
|
||||||
branding_default_flow_background:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
flow_authentication:
|
flow_authentication:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
@ -41282,6 +41310,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-captcha
|
default: ak-stage-captcha
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -41304,6 +41334,7 @@ components:
|
|||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
- site_key
|
- site_key
|
||||||
|
- xid
|
||||||
CaptchaChallengeResponseRequest:
|
CaptchaChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Validate captcha token
|
description: Validate captcha token
|
||||||
@ -41687,6 +41718,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-consent
|
default: ak-stage-consent
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -41715,6 +41748,7 @@ components:
|
|||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
- permissions
|
- permissions
|
||||||
- token
|
- token
|
||||||
|
- xid
|
||||||
ConsentChallengeResponseRequest:
|
ConsentChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Consent challenge response, any valid response request is valid
|
description: Consent challenge response, any valid response request is valid
|
||||||
@ -42109,8 +42143,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
branding_favicon:
|
branding_favicon:
|
||||||
type: string
|
type: string
|
||||||
branding_custom_css:
|
|
||||||
type: string
|
|
||||||
ui_footer_links:
|
ui_footer_links:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -42137,7 +42169,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- branding_custom_css
|
|
||||||
- branding_favicon
|
- branding_favicon
|
||||||
- branding_logo
|
- branding_logo
|
||||||
- branding_title
|
- branding_title
|
||||||
@ -42491,6 +42522,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-dummy
|
default: ak-stage-dummy
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -42501,6 +42534,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
- xid
|
||||||
DummyChallengeResponseRequest:
|
DummyChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Dummy challenge response
|
description: Dummy challenge response
|
||||||
@ -42693,12 +42727,16 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-email
|
default: ak-stage-email
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ErrorDetail'
|
$ref: '#/components/schemas/ErrorDetail'
|
||||||
|
required:
|
||||||
|
- xid
|
||||||
EmailChallengeResponseRequest:
|
EmailChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: |-
|
description: |-
|
||||||
@ -43617,6 +43655,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-flow-error
|
default: ak-stage-flow-error
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -43631,6 +43671,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- request_id
|
- request_id
|
||||||
|
- xid
|
||||||
FlowImportResult:
|
FlowImportResult:
|
||||||
type: object
|
type: object
|
||||||
description: Logs of an attempted flow import
|
description: Logs of an attempted flow import
|
||||||
@ -43945,6 +43986,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: xak-flow-frame
|
default: xak-flow-frame
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -43961,6 +44004,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- loading_text
|
- loading_text
|
||||||
- url
|
- url
|
||||||
|
- xid
|
||||||
FrameChallengeResponseRequest:
|
FrameChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Base class for all challenge responses
|
description: Base class for all challenge responses
|
||||||
@ -44763,6 +44807,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-identification
|
default: ak-stage-identification
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -44807,6 +44853,7 @@ components:
|
|||||||
- primary_action
|
- primary_action
|
||||||
- show_source_labels
|
- show_source_labels
|
||||||
- user_fields
|
- user_fields
|
||||||
|
- xid
|
||||||
IdentificationChallengeResponseRequest:
|
IdentificationChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Identification challenge
|
description: Identification challenge
|
||||||
@ -46890,18 +46937,10 @@ components:
|
|||||||
webhook_url:
|
webhook_url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
webhook_mapping_body:
|
webhook_mapping:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Customize the body of the request. Mapping should return data
|
|
||||||
that is JSON-serializable.
|
|
||||||
webhook_mapping_headers:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
description: Configure additional headers to be sent. Mapping should return
|
|
||||||
a dictionary of key-value pairs
|
|
||||||
send_once:
|
send_once:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Only send notification once, for example when sending a webhook
|
description: Only send notification once, for example when sending a webhook
|
||||||
@ -46929,18 +46968,10 @@ components:
|
|||||||
webhook_url:
|
webhook_url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
webhook_mapping_body:
|
webhook_mapping:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Customize the body of the request. Mapping should return data
|
|
||||||
that is JSON-serializable.
|
|
||||||
webhook_mapping_headers:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
description: Configure additional headers to be sent. Mapping should return
|
|
||||||
a dictionary of key-value pairs
|
|
||||||
send_once:
|
send_once:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Only send notification once, for example when sending a webhook
|
description: Only send notification once, for example when sending a webhook
|
||||||
@ -47265,12 +47296,16 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-provider-oauth2-device-code
|
default: ak-provider-oauth2-device-code
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ErrorDetail'
|
$ref: '#/components/schemas/ErrorDetail'
|
||||||
|
required:
|
||||||
|
- xid
|
||||||
OAuthDeviceCodeChallengeResponseRequest:
|
OAuthDeviceCodeChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Response that includes the user-entered device code
|
description: Response that includes the user-entered device code
|
||||||
@ -47293,12 +47328,16 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-provider-oauth2-device-code-finish
|
default: ak-provider-oauth2-device-code-finish
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ErrorDetail'
|
$ref: '#/components/schemas/ErrorDetail'
|
||||||
|
required:
|
||||||
|
- xid
|
||||||
OAuthDeviceCodeFinishChallengeResponseRequest:
|
OAuthDeviceCodeFinishChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Response that device has been authenticated and tab can be closed
|
description: Response that device has been authenticated and tab can be closed
|
||||||
@ -49443,6 +49482,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-password
|
default: ak-stage-password
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -49461,6 +49502,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
PasswordChallengeResponseRequest:
|
PasswordChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Password challenge response
|
description: Password challenge response
|
||||||
@ -50157,11 +50199,6 @@ components:
|
|||||||
branding_favicon:
|
branding_favicon:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
branding_custom_css:
|
|
||||||
type: string
|
|
||||||
branding_default_flow_background:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
flow_authentication:
|
flow_authentication:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
@ -51374,18 +51411,10 @@ components:
|
|||||||
webhook_url:
|
webhook_url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
webhook_mapping_body:
|
webhook_mapping:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Customize the body of the request. Mapping should return data
|
|
||||||
that is JSON-serializable.
|
|
||||||
webhook_mapping_headers:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
description: Configure additional headers to be sent. Mapping should return
|
|
||||||
a dictionary of key-value pairs
|
|
||||||
send_once:
|
send_once:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Only send notification once, for example when sending a webhook
|
description: Only send notification once, for example when sending a webhook
|
||||||
@ -52245,8 +52274,9 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
|
||||||
format: uri
|
format: uri
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -52403,14 +52433,16 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: URL that the initial Login request is sent to.
|
description: URL that the initial Login request is sent to.
|
||||||
format: uri
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -53032,6 +53064,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-source-plex
|
default: ak-source-plex
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -53045,6 +53079,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- client_id
|
- client_id
|
||||||
- slug
|
- slug
|
||||||
|
- xid
|
||||||
PlexAuthenticationChallengeResponseRequest:
|
PlexAuthenticationChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Pseudo class for plex response
|
description: Pseudo class for plex response
|
||||||
@ -53557,6 +53592,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-prompt
|
default: ak-stage-prompt
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -53569,6 +53606,7 @@ components:
|
|||||||
$ref: '#/components/schemas/StagePrompt'
|
$ref: '#/components/schemas/StagePrompt'
|
||||||
required:
|
required:
|
||||||
- fields
|
- fields
|
||||||
|
- xid
|
||||||
PromptChallengeResponseRequest:
|
PromptChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: |-
|
description: |-
|
||||||
@ -54753,6 +54791,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: xak-flow-redirect
|
default: xak-flow-redirect
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -54763,6 +54803,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- to
|
- to
|
||||||
|
- xid
|
||||||
RedirectChallengeResponseRequest:
|
RedirectChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Redirect challenge response
|
description: Redirect challenge response
|
||||||
@ -55211,6 +55252,7 @@ components:
|
|||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -55377,8 +55419,9 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
|
||||||
format: uri
|
format: uri
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -55551,13 +55594,15 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
description: URL that the initial Login request is sent to.
|
|
||||||
format: uri
|
format: uri
|
||||||
|
description: URL that the initial Login request is sent to.
|
||||||
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -55740,14 +55785,16 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: URL that the initial Login request is sent to.
|
description: URL that the initial Login request is sent to.
|
||||||
format: uri
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -56652,6 +56699,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-session-end
|
default: ak-stage-session-end
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -56674,6 +56723,7 @@ components:
|
|||||||
- brand_name
|
- brand_name
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
SessionUser:
|
SessionUser:
|
||||||
type: object
|
type: object
|
||||||
description: |-
|
description: |-
|
||||||
@ -56786,6 +56836,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: xak-flow-shell
|
default: xak-flow-shell
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -56796,6 +56848,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- body
|
- body
|
||||||
|
- xid
|
||||||
SignatureAlgorithmEnum:
|
SignatureAlgorithmEnum:
|
||||||
enum:
|
enum:
|
||||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||||
@ -58070,6 +58123,8 @@ components:
|
|||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
default: ak-stage-user-login
|
default: ak-stage-user-login
|
||||||
|
xid:
|
||||||
|
type: string
|
||||||
response_errors:
|
response_errors:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -58083,6 +58138,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
|
- xid
|
||||||
UserLoginChallengeResponseRequest:
|
UserLoginChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User login challenge
|
description: User login challenge
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user