Compare commits
220 Commits
feature/so
...
static-con
Author | SHA1 | Date | |
---|---|---|---|
86c1d60093 | |||
e5e53f034e | |||
71b87127d1 | |||
d5d67fe22d | |||
5d2685341d | |||
f1ac4ff9c9 | |||
79f4c66286 | |||
1f82094c0b | |||
35440acba3 | |||
eca9901704 | |||
6ddd5a3d5f | |||
5664e62eca | |||
1403f17d62 | |||
1ac8989e81 | |||
b0a1db77e3 | |||
46da4cb59e | |||
154df5cdf7 | |||
5b889456f6 | |||
3eaed82c48 | |||
feaf9d8bc9 | |||
2899668ae2 | |||
4c25e1bb24 | |||
464ff3f5b1 | |||
22eb5f56f1 | |||
7e48e87f49 | |||
8ce12f7850 | |||
2514baabeb | |||
945930a507 | |||
537a80ad97 | |||
5c993e23fe | |||
eb2db18494 | |||
12a46a8426 | |||
4a1213310a | |||
84c2097148 | |||
c05dedc573 | |||
18c197e75b | |||
0c26a0bce2 | |||
5fd6a4cead | |||
51fb1bd8e7 | |||
4a30f87a42 | |||
8e6b6ede30 | |||
af30c2a68e | |||
9b65627a3e | |||
4bad91c901 | |||
f3c479d077 | |||
b024df9903 | |||
f6a6458088 | |||
f0dc0e8900 | |||
79e89b0376 | |||
4cc7d91379 | |||
245909e31a | |||
997a1ddb3d | |||
42335a60bf | |||
fc539332e1 | |||
d9efb02078 | |||
6212250e19 | |||
c18beefc8f | |||
f23da6e402 | |||
e934b246c8 | |||
ead684a410 | |||
d782aadab7 | |||
4ac6f83aea | |||
6281d36a69 | |||
8129ad4ec0 | |||
24eea415b2 | |||
a615ce8e95 | |||
5b275cf7fb | |||
d6e91c119f | |||
7841e47e74 | |||
ad2a4bea3e | |||
a554c085c1 | |||
ff0d978754 | |||
de48e62819 | |||
e50e995d2f | |||
3bf4156cb3 | |||
89990facf5 | |||
48545950ed | |||
0544aa5fae | |||
5d69455b87 | |||
3d291cf4da | |||
44d7c42dc7 | |||
4ea4e925e3 | |||
169172c85f | |||
adea637fa4 | |||
0231277d9c | |||
45643ed1f6 | |||
3823d56dbd | |||
43cfd59ac0 | |||
c8555bbf59 | |||
a4251a3410 | |||
50985f9b0b | |||
9ec24528d4 | |||
5eac38c0cc | |||
010df0c31c | |||
7ba858eff3 | |||
817d2d5ff8 | |||
70e34e03b4 | |||
d61f9f6d57 | |||
bdf81706b8 | |||
7b56602fc9 | |||
7c6e25a996 | |||
0eeaeaf1ff | |||
9ce4337b11 | |||
c6a3c7371c | |||
42a7cf10f2 | |||
bb4f7b1193 | |||
3eecfb835b | |||
92ab856bd3 | |||
178549a756 | |||
67d178aa11 | |||
ef53abace9 | |||
5effb3a0f6 | |||
3a37916a8f | |||
428d5ac9cf | |||
7b4037fdda | |||
2c7bbcc27b | |||
19fb24de99 | |||
2709702896 | |||
7d0d5a7dc2 | |||
6a04a2ca69 | |||
ea561c9da6 | |||
9b9c55f17c | |||
bd5e78bd44 | |||
ab98028022 | |||
813ff64ba1 | |||
c99e742214 | |||
dac6ad3cd6 | |||
e4d2a53ccc | |||
3b6775fd9c | |||
5882e0b2cb | |||
65f0b471d8 | |||
7d054db1a5 | |||
cb75ba2e5e | |||
36cecc1391 | |||
81b91d8777 | |||
41dc23b3c2 | |||
370eff1494 | |||
0ff8def03b | |||
b01cafd9fe | |||
90aa8abb80 | |||
fd21aae4f9 | |||
360223a2ff | |||
0e83de2697 | |||
a23bac9d9b | |||
220378b3f2 | |||
363d655378 | |||
e93b2a1a75 | |||
76665cf65e | |||
3ad7f4dc24 | |||
c5045e8792 | |||
a8c9b3a8ba | |||
148506639a | |||
53814d9919 | |||
08b04c32f5 | |||
1c1d97339d | |||
cafa9c1737 | |||
5f64347ba1 | |||
45ef54480a | |||
a3dc8af4c6 | |||
36933a0aca | |||
8f689890df | |||
ec49b2e0e0 | |||
22ebe05706 | |||
f0e58a6f49 | |||
a3d642c08e | |||
5d42cb9185 | |||
1fd0cc5bb5 | |||
deef365ff5 | |||
d1ae6287f2 | |||
2e152cd264 | |||
f5941e403b | |||
ff3cf8c10e | |||
bfa6328172 | |||
4c9691c932 | |||
a0f1566b4c | |||
46261a4f42 | |||
8b42ff1e97 | |||
ca4cb0d251 | |||
a5a0fa79dd | |||
c06a871f61 | |||
4a3df67134 | |||
422ccf61fa | |||
d989f23907 | |||
059180edef | |||
22f30634a8 | |||
35ff418c42 | |||
7826e7a605 | |||
64f1b8207d | |||
b2c13f0614 | |||
6965628020 | |||
608f63e9a2 | |||
22fa3a7fba | |||
bcfd6fefa7 | |||
eae18d0016 | |||
4a12a57c5f | |||
71294b7deb | |||
5af907db0c | |||
63a118a2ba | |||
d9a3c34a44 | |||
23bdad7574 | |||
8ee90826fc | |||
8c7d4d2f5e | |||
d72def0368 | |||
5bcf501842 | |||
13fc216c68 | |||
27aed4b315 | |||
84b5992e55 | |||
7eb985f636 | |||
d3172ae904 | |||
88662b54c1 | |||
b38bc8c1c4 | |||
a9b648842a | |||
5fda531e2b | |||
921a3e6eb8 | |||
fd898bea66 | |||
cbf9ee55ae | |||
590ee7d9d4 | |||
b8cd1d1ae2 | |||
9f9524fbcb | |||
1df87cdf77 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.2
|
||||
current_version = 2025.2.4
|
||||
tag = 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*))?
|
||||
@ -17,6 +17,8 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:pyproject.toml]
|
||||
|
||||
[bumpversion:file:uv.lock]
|
||||
|
||||
[bumpversion:file:package.json]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
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,7 +44,6 @@ if is_release:
|
||||
]
|
||||
if not prerelease:
|
||||
image_tags += [
|
||||
f"{name}:latest",
|
||||
f"{name}:{version_family}",
|
||||
]
|
||||
else:
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 5000s --verbose
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -33,6 +33,7 @@ eggs/
|
||||
lib64/
|
||||
parts/
|
||||
dist/
|
||||
out/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
|
@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
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 \
|
||||
--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 && \
|
||||
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
go build -o /go/authentik ./cmd/server
|
||||
|
||||
# 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"
|
||||
|
||||
# Stage 5: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.9 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.14 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.2"
|
||||
__version__ = "2025.2.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -46,7 +46,7 @@ LOGGER = get_logger()
|
||||
|
||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||
"""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:
|
||||
key += f"/{page_number}"
|
||||
return key
|
||||
|
@ -179,10 +179,13 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"user",
|
||||
"source",
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -199,7 +202,7 @@ class UserSourceConnectionViewSet(
|
||||
queryset = UserSourceConnection.objects.all()
|
||||
serializer_class = UserSourceConnectionSerializer
|
||||
filterset_fields = ["user", "source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
search_fields = ["user__username", "source__slug", "identifier"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
||||
@ -218,9 +221,11 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -237,6 +242,5 @@ class GroupSourceConnectionViewSet(
|
||||
queryset = GroupSourceConnection.objects.all()
|
||||
serializer_class = GroupSourceConnectionSerializer
|
||||
filterset_fields = ["group", "source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
search_fields = ["group__name", "source__slug", "identifier"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""User API Views"""
|
||||
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
class UserGroupSerializer(ModelSerializer):
|
||||
@ -226,6 +228,7 @@ class UserSerializer(ModelSerializer):
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"groups_obj",
|
||||
@ -240,6 +243,7 @@ class UserSerializer(ModelSerializer):
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"allow_blank": True},
|
||||
"date_joined": {"read_only": True},
|
||||
"password_change_date": {"read_only": True},
|
||||
}
|
||||
|
||||
@ -373,7 +377,7 @@ class UsersFilter(FilterSet):
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(field_name="path")
|
||||
@ -391,6 +395,11 @@ class UsersFilter(FilterSet):
|
||||
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):
|
||||
"""Filter attributes by query args"""
|
||||
try:
|
||||
@ -769,7 +778,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
if not instance.is_active:
|
||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||
session_ids = sessions.values_list("session_key", flat=True)
|
||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||
for session in session_ids:
|
||||
SessionStore(session).delete()
|
||||
sessions.delete()
|
||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||
return response
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.13 on 2025-04-07 14:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0043_alter_group_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="usersourceconnection",
|
||||
name="new_identifier",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -0,0 +1,30 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
|
||||
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
|
||||
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
|
||||
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="usersourceconnection",
|
||||
old_name="new_identifier",
|
||||
new_name="identifier",
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usersourceconnection",
|
||||
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usersourceconnection",
|
||||
index=models.Index(
|
||||
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
|
||||
),
|
||||
),
|
||||
]
|
@ -761,11 +761,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
@property
|
||||
def component(self) -> str:
|
||||
"""Return component used to edit this object"""
|
||||
if self.managed == self.MANAGED_INBUILT:
|
||||
return ""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||
"""Return property mapping type used by this object"""
|
||||
if self.managed == self.MANAGED_INBUILT:
|
||||
from authentik.core.models import PropertyMapping
|
||||
|
||||
return PropertyMapping
|
||||
raise NotImplementedError
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||
@ -780,10 +786,14 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
|
||||
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||
"""Get base properties for a user to build final properties upon."""
|
||||
if self.managed == self.MANAGED_INBUILT:
|
||||
return {}
|
||||
raise NotImplementedError
|
||||
|
||||
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||
"""Get base properties for a group to build final properties upon."""
|
||||
if self.managed == self.MANAGED_INBUILT:
|
||||
return {}
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
@ -814,6 +824,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||
identifier = models.TextField()
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@ -827,6 +838,10 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = (("user", "source"),)
|
||||
indexes = (
|
||||
models.Index(fields=("identifier",)),
|
||||
models.Index(fields=("source", "identifier")),
|
||||
)
|
||||
|
||||
|
||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""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.sessions.backends.cache import KEY_PREFIX
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
@ -25,6 +28,7 @@ password_changed = Signal()
|
||||
login_failed = Signal()
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@receiver(post_save, sender=Application)
|
||||
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
"""Delete session when authenticated session is deleted"""
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
SessionStore(instance.session_key).delete()
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
|
@ -48,6 +48,7 @@ LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||
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
|
||||
|
||||
|
||||
@ -261,6 +262,7 @@ class SourceFlowManager:
|
||||
plan.append_stage(stage)
|
||||
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||
plan.append_stage(stage)
|
||||
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
|
||||
return plan.to_redirect(self.request, flow)
|
||||
|
||||
def handle_auth(
|
||||
|
19
authentik/core/tests/test_source_api.py
Normal file
19
authentik/core/tests/test_source_api.py
Normal file
@ -0,0 +1,19 @@
|
||||
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,6 +1,7 @@
|
||||
"""Test Users API"""
|
||||
|
||||
from datetime import datetime
|
||||
from json import loads
|
||||
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
@ -15,7 +16,12 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||
from authentik.core.tests.utils import (
|
||||
create_test_admin_user,
|
||||
create_test_brand,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@ -26,7 +32,7 @@ class TestUsersAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
self.user = create_test_user()
|
||||
|
||||
def test_filter_type(self):
|
||||
"""Test API filtering by type"""
|
||||
@ -41,6 +47,35 @@ class TestUsersAPI(APITestCase):
|
||||
)
|
||||
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):
|
||||
"""Test listing with groups"""
|
||||
self.client.force_login(self.admin)
|
||||
@ -99,6 +134,8 @@ class TestUsersAPI(APITestCase):
|
||||
def test_recovery_email_no_flow(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
self.client.force_login(self.admin)
|
||||
self.user.email = ""
|
||||
self.user.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||
)
|
||||
|
@ -13,7 +13,11 @@ from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||
from authentik.core.api.groups import GroupViewSet
|
||||
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
||||
from authentik.core.api.providers import ProviderViewSet
|
||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionViewSet,
|
||||
SourceViewSet,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||
from authentik.core.api.users import UserViewSet
|
||||
@ -81,6 +85,7 @@ api_urlpatterns = [
|
||||
("core/tokens", TokenViewSet),
|
||||
("sources/all", SourceViewSet),
|
||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||
("sources/group_connections/all", GroupSourceConnectionViewSet),
|
||||
("providers/all", ProviderViewSet),
|
||||
("propertymappings/all", PropertyMappingViewSet),
|
||||
("authenticators/all", DeviceViewSet, "device"),
|
||||
|
@ -11,13 +11,14 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from authentik.core.models import Source, User
|
||||
from authentik.core.sources.flow_manager import (
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
||||
SESSION_KEY_SOURCE_FLOW_CONTEXT,
|
||||
SESSION_KEY_SOURCE_FLOW_STAGES,
|
||||
)
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.models import FlowToken, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.stage import ChallengeStageView, StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
@ -53,6 +54,9 @@ class SourceStageView(ChallengeStageView):
|
||||
resume_token = self.create_flow_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_CONTEXT] = {
|
||||
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
|
||||
}
|
||||
return self.login_button.challenge
|
||||
|
||||
def create_flow_token(self) -> FlowToken:
|
||||
|
@ -69,6 +69,7 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
||||
SESSION_KEY_GET = "authentik/flows/get"
|
||||
SESSION_KEY_POST = "authentik/flows/post"
|
||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
|
||||
QS_KEY_TOKEN = "flow_token" # nosec
|
||||
QS_QUERY = "query"
|
||||
|
||||
@ -453,6 +454,7 @@ class FlowExecutorView(APIView):
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_PLAN,
|
||||
SESSION_KEY_GET,
|
||||
SESSION_KEY_AUTH_STARTED,
|
||||
# We might need the initial POST payloads for later requests
|
||||
# SESSION_KEY_POST,
|
||||
# We don't delete the history on purpose, as a user might
|
||||
|
@ -6,14 +6,22 @@ from django.shortcuts import get_object_or_404
|
||||
from ua_parser.user_agent_parser import Parse
|
||||
|
||||
from authentik.core.views.interface import InterfaceView
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
|
||||
|
||||
|
||||
class FlowInterfaceView(InterfaceView):
|
||||
"""Flow interface"""
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
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
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
@ -1,5 +1,20 @@
|
||||
# update website/docs/install-config/configuration/configuration.mdx
|
||||
# This is the default configuration file
|
||||
# authentik configuration
|
||||
#
|
||||
# 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:
|
||||
host: localhost
|
||||
name: authentik
|
||||
|
@ -18,6 +18,15 @@ class SerializerModel(models.Model):
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
"""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
|
||||
|
||||
|
||||
|
@ -13,6 +13,7 @@ from paramiko.ssh_exception import SSHException
|
||||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||
@ -184,7 +185,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
self.client.images.pull(image)
|
||||
except DockerException: # pragma: no cover
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
||||
self.client.images.pull(image)
|
||||
return image
|
||||
|
||||
|
@ -35,3 +35,4 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
89
authentik/policies/templates/policies/buffer.html
Normal file
89
authentik/policies/templates/policies/buffer.html
Normal file
@ -0,0 +1,89 @@
|
||||
{% 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 %}
|
121
authentik/policies/tests/test_views.py
Normal file
121
authentik/policies/tests/test_views.py
Normal file
@ -0,0 +1,121 @@
|
||||
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,7 +1,14 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||
from authentik.policies.api.policies import PolicyViewSet
|
||||
from authentik.policies.views import BufferView
|
||||
|
||||
urlpatterns = [
|
||||
path("buffer", BufferView.as_view(), name="buffer"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("policies/all", PolicyViewSet),
|
||||
|
@ -1,23 +1,37 @@
|
||||
"""authentik access helper classes"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
||||
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.views.generic.base import View
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, Provider, User
|
||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
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.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
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):
|
||||
@ -125,3 +139,65 @@ class PolicyAccessView(AccessMixin, View):
|
||||
for message in result.messages:
|
||||
messages.error(self.request, _(message))
|
||||
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.views import bad_request_message
|
||||
from authentik.policies.types import PolicyRequest
|
||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
|
||||
from authentik.providers.oauth2.constants import (
|
||||
PKCE_METHOD_PLAIN,
|
||||
PKCE_METHOD_S256,
|
||||
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
|
||||
return code
|
||||
|
||||
|
||||
class AuthorizationFlowInitView(PolicyAccessView):
|
||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
||||
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
||||
|
||||
params: OAuthAuthorizationParams
|
||||
|
@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||
from authentik.flows.stage import RedirectStage
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.policies.views import BufferedPolicyAccessView
|
||||
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||
|
||||
|
||||
class RACStartView(PolicyAccessView):
|
||||
class RACStartView(BufferedPolicyAccessView):
|
||||
"""Start a RAC connection by checking access and creating a connection token"""
|
||||
|
||||
endpoint: Endpoint
|
||||
|
@ -0,0 +1,22 @@
|
||||
# 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,6 +10,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.api.object_types import CreatableType
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.models import DomainlessURLValidator
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
@ -40,7 +41,9 @@ class SAMLBindings(models.TextChoices):
|
||||
class SAMLProvider(Provider):
|
||||
"""SAML 2.0 Endpoint for applications which support SAML."""
|
||||
|
||||
acs_url = models.URLField(verbose_name=_("ACS URL"))
|
||||
acs_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
|
||||
)
|
||||
audience = models.TextField(
|
||||
default="",
|
||||
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.views.executor import SESSION_KEY_POST
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.policies.views import BufferedPolicyAccessView
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class SAMLSSOView(PolicyAccessView):
|
||||
class SAMLSSOView(BufferedPolicyAccessView):
|
||||
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
||||
Calls get/post handler."""
|
||||
|
||||
@ -83,7 +83,7 @@ class SAMLSSOView(PolicyAccessView):
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
"""GET and POST use the same handler, but we can't
|
||||
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
|
||||
return self.get(request, application_slug)
|
||||
|
||||
|
||||
|
@ -28,8 +28,8 @@ def pytest_report_header(*_, **__):
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
||||
current_id = int(environ.get("CI_RUN_ID", 0)) - 1
|
||||
total_ids = int(environ.get("CI_TOTAL_RUNS", 0))
|
||||
current_id = int(environ.get("CI_RUN_ID", "0")) - 1
|
||||
total_ids = int(environ.get("CI_TOTAL_RUNS", "0"))
|
||||
|
||||
if total_ids:
|
||||
num_tests = len(items)
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
UserSourceConnectionSerializer,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.kerberos.models import (
|
||||
GroupKerberosSourceConnection,
|
||||
UserKerberosSourceConnection,
|
||||
@ -15,33 +13,20 @@ from authentik.sources.kerberos.models import (
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserKerberosSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = UserKerberosSourceConnection.objects.all()
|
||||
serializer_class = UserKerberosSourceConnectionSerializer
|
||||
filterset_fields = ["source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
ordering = ["source__slug"]
|
||||
owner_field = "user"
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupKerberosSourceConnection
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupKerberosSourceConnection.objects.all()
|
||||
serializer_class = GroupKerberosSourceConnectionSerializer
|
||||
|
@ -0,0 +1,28 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserKerberosSourceConnection = apps.get_model(
|
||||
"authentik_sources_kerberos", "UserKerberosSourceConnection"
|
||||
)
|
||||
|
||||
for connection in UserKerberosSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_kerberos", "0002_kerberossource_kadmin_type"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="userkerberossourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -372,8 +372,6 @@ class KerberosSourcePropertyMapping(PropertyMapping):
|
||||
class UserKerberosSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured Kerberos Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.source_connection import (
|
||||
|
@ -99,6 +99,7 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||
"sync_groups",
|
||||
"sync_parent_group",
|
||||
"connectivity",
|
||||
"lookup_groups_from_user",
|
||||
]
|
||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||
|
||||
@ -134,6 +135,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"sync_parent_group",
|
||||
"user_property_mappings",
|
||||
"group_property_mappings",
|
||||
"lookup_groups_from_user",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-26 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_sources_ldap",
|
||||
"0006_rename_ldappropertymapping_ldapsourcepropertymapping_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ldapsource",
|
||||
name="lookup_groups_from_user",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory",
|
||||
),
|
||||
),
|
||||
]
|
@ -123,6 +123,14 @@ class LDAPSource(Source):
|
||||
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
|
||||
lookup_groups_from_user = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Lookup group membership based on a user attribute instead of a group attribute. "
|
||||
"This allows nested group resolution on systems like FreeIPA and Active Directory"
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-source-ldap-form"
|
||||
|
@ -28,15 +28,17 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
if not self._source.sync_groups:
|
||||
self.message("Group syncing is disabled for this Source")
|
||||
return iter(())
|
||||
|
||||
# If we are looking up groups from users, we don't need to fetch the group membership field
|
||||
attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
|
||||
if not self._source.lookup_groups_from_user:
|
||||
attributes.append(self._source.group_membership_field)
|
||||
|
||||
return self.search_paginator(
|
||||
search_base=self.base_dn_groups,
|
||||
search_filter=self._source.group_object_filter,
|
||||
search_scope=SUBTREE,
|
||||
attributes=[
|
||||
self._source.group_membership_field,
|
||||
self._source.object_uniqueness_field,
|
||||
LDAP_DISTINGUISHED_NAME,
|
||||
],
|
||||
attributes=attributes,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@ -47,9 +49,24 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
return -1
|
||||
membership_count = 0
|
||||
for group in page_data:
|
||||
if "attributes" not in group:
|
||||
continue
|
||||
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
|
||||
if self._source.lookup_groups_from_user:
|
||||
group_dn = group.get("dn", {})
|
||||
group_filter = f"({self._source.group_membership_field}={group_dn})"
|
||||
group_members = self._source.connection().extend.standard.paged_search(
|
||||
search_base=self.base_dn_users,
|
||||
search_filter=group_filter,
|
||||
search_scope=SUBTREE,
|
||||
attributes=[self._source.object_uniqueness_field],
|
||||
)
|
||||
members = []
|
||||
for group_member in group_members:
|
||||
group_member_dn = group_member.get("dn", {})
|
||||
members.append(group_member_dn)
|
||||
else:
|
||||
if "attributes" not in group:
|
||||
continue
|
||||
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
|
||||
|
||||
ak_group = self.get_group(group)
|
||||
if not ak_group:
|
||||
continue
|
||||
@ -68,7 +85,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"ak_groups__in": [ak_group],
|
||||
}
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
membership_count += 1
|
||||
membership_count += users.count()
|
||||
ak_group.users.set(users)
|
||||
|
@ -96,6 +96,26 @@ def mock_freeipa_connection(password: str) -> Connection:
|
||||
"objectClass": "posixAccount",
|
||||
},
|
||||
)
|
||||
# User with groups in memberOf attribute
|
||||
connection.strategy.add_entry(
|
||||
"cn=user4,ou=users,dc=goauthentik,dc=io",
|
||||
{
|
||||
"name": "user4_sn",
|
||||
"uid": "user4_sn",
|
||||
"objectClass": "person",
|
||||
"memberOf": [
|
||||
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
|
||||
],
|
||||
},
|
||||
)
|
||||
connection.strategy.add_entry(
|
||||
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
|
||||
{
|
||||
"cn": "reverse-lookup-group",
|
||||
"uid": "reverse-lookup-group",
|
||||
"objectClass": "groupOfNames",
|
||||
},
|
||||
)
|
||||
# Locked out user
|
||||
connection.strategy.add_entry(
|
||||
"cn=user-nsaccountlock,ou=users,dc=goauthentik,dc=io",
|
||||
|
@ -162,6 +162,43 @@ class LDAPSyncTests(TestCase):
|
||||
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
||||
self.assertFalse(User.objects.get(username="user-nsaccountlock").is_active)
|
||||
|
||||
def test_sync_groups_freeipa_memberOf(self):
|
||||
"""Test group sync when membership is derived from memberOf user attribute"""
|
||||
self.source.object_uniqueness_field = "uid"
|
||||
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
||||
self.source.lookup_groups_from_user = True
|
||||
self.source.group_membership_field = "memberOf"
|
||||
self.source.user_property_mappings.set(
|
||||
LDAPSourcePropertyMapping.objects.filter(
|
||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||
)
|
||||
)
|
||||
self.source.group_property_mappings.set(
|
||||
LDAPSourcePropertyMapping.objects.filter(
|
||||
managed="goauthentik.io/sources/ldap/openldap-cn"
|
||||
)
|
||||
)
|
||||
connection = MagicMock(return_value=mock_freeipa_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source)
|
||||
user_sync.sync_full()
|
||||
group_sync = GroupLDAPSynchronizer(self.source)
|
||||
group_sync.sync_full()
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source)
|
||||
membership_sync.sync_full()
|
||||
|
||||
self.assertTrue(
|
||||
User.objects.filter(username="user4_sn").exists(), "User does not exist"
|
||||
)
|
||||
# Test if membership mapping based on memberOf works.
|
||||
memberof_group = Group.objects.filter(name="reverse-lookup-group")
|
||||
self.assertTrue(memberof_group.exists(), "Group does not exist")
|
||||
self.assertTrue(
|
||||
memberof_group.first().users.filter(username="user4_sn").exists(),
|
||||
"User not a member of the group",
|
||||
)
|
||||
|
||||
def test_sync_groups_ad(self):
|
||||
"""Test group sync"""
|
||||
self.source.user_property_mappings.set(
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,11 +10,9 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
|
||||
|
||||
|
||||
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserOAuthSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier", "access_token"]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"access_token": {"write_only": True},
|
||||
@ -24,21 +20,15 @@ class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
|
||||
|
||||
class UserOAuthSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserOAuthSourceConnection.objects.all()
|
||||
serializer_class = UserOAuthSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupOAuthSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupOAuthSourceConnection
|
||||
|
||||
|
||||
class GroupOAuthSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupOAuthSourceConnection.objects.all()
|
||||
serializer_class = GroupOAuthSourceConnectionSerializer
|
||||
|
@ -0,0 +1,28 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserOAuthSourceConnection = apps.get_model(
|
||||
"authentik_sources_oauth", "UserOAuthSourceConnection"
|
||||
)
|
||||
|
||||
for connection in UserOAuthSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_oauth", "0008_groupoauthsourceconnection_and_more"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="useroauthsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -286,7 +286,6 @@ class OAuthSourcePropertyMapping(PropertyMapping):
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
identifier = models.CharField(max_length=255)
|
||||
access_token = models.TextField(blank=True, null=True, default=None)
|
||||
|
||||
@property
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,14 +10,9 @@ from authentik.sources.plex.models import GroupPlexSourceConnection, UserPlexSou
|
||||
|
||||
|
||||
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserPlexSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + [
|
||||
"identifier",
|
||||
"plex_token",
|
||||
]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["plex_token"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"plex_token": {"write_only": True},
|
||||
@ -27,21 +20,15 @@ class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
|
||||
|
||||
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
queryset = UserPlexSourceConnection.objects.all()
|
||||
serializer_class = UserPlexSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""Plex Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupPlexSourceConnection
|
||||
|
||||
|
||||
class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupPlexSourceConnection.objects.all()
|
||||
serializer_class = GroupPlexSourceConnectionSerializer
|
||||
|
@ -0,0 +1,29 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserPlexSourceConnection = apps.get_model("authentik_sources_plex", "UserPlexSourceConnection")
|
||||
|
||||
for connection in UserPlexSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_sources_plex",
|
||||
"0004_groupplexsourceconnection_plexsourcepropertymapping_and_more",
|
||||
),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="userplexsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -141,7 +141,6 @@ class UserPlexSourceConnection(UserSourceConnection):
|
||||
"""Connect user and plex source"""
|
||||
|
||||
plex_token = models.TextField()
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,29 +10,20 @@ from authentik.sources.saml.models import GroupSAMLSourceConnection, UserSAMLSou
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserSAMLSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserSAMLSourceConnection.objects.all()
|
||||
serializer_class = UserSAMLSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupSAMLSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupSAMLSourceConnection
|
||||
|
||||
|
||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupSAMLSourceConnection.objects.all()
|
||||
serializer_class = GroupSAMLSourceConnectionSerializer
|
||||
|
@ -0,0 +1,35 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,26 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserSAMLSourceConnection = apps.get_model("authentik_sources_saml", "UserSAMLSourceConnection")
|
||||
|
||||
for connection in UserSAMLSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_saml", "0018_alter_samlsource_slo_url_alter_samlsource_sso_url"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="usersamlsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -20,6 +20,7 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.challenge import RedirectChallenge
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.lib.models import DomainlessURLValidator
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
@ -91,11 +92,13 @@ class SAMLSource(Source):
|
||||
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
||||
)
|
||||
|
||||
sso_url = models.URLField(
|
||||
sso_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name=_("SSO URL"),
|
||||
help_text=_("URL that the initial Login request is sent to."),
|
||||
)
|
||||
slo_url = models.URLField(
|
||||
slo_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -315,8 +318,6 @@ class SAMLSourcePropertyMapping(PropertyMapping):
|
||||
class UserSAMLSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured SAML Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer
|
||||
|
File diff suppressed because one or more lines are too long
@ -104,6 +104,13 @@ def send_mail(
|
||||
# can't be converted to json)
|
||||
message_object.attach(logo_data())
|
||||
|
||||
if (
|
||||
message_object.to
|
||||
and isinstance(message_object.to[0], str)
|
||||
and "=?utf-8?" in message_object.to[0]
|
||||
):
|
||||
message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")]
|
||||
|
||||
LOGGER.debug("Sending mail", to=message_object.to)
|
||||
backend.send_messages([message_object])
|
||||
Event.new(
|
||||
|
@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
@ -67,6 +67,67 @@ class TestEmailStageSending(FlowTestCase):
|
||||
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
||||
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_utf8_name(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
utf8_user = create_test_user()
|
||||
utf8_user.name = "Cirilo ЉМНЊ el cirilico И̂ӢЙӤ "
|
||||
utf8_user.email = "cyrillic@authentik.local"
|
||||
utf8_user.save()
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = utf8_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"{utf8_user.email}"])
|
||||
|
||||
def test_pending_fake_user(self):
|
||||
"""Test with pending (fake) user"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
|
@ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
sanitized_to = []
|
||||
# Ensure that all recipients are valid
|
||||
for recipient_name, recipient_email in to:
|
||||
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
||||
# Remove any newline characters from name and email before sanitizing
|
||||
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)
|
||||
if not template_name:
|
||||
return
|
||||
|
@ -142,35 +142,38 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
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
|
||||
if captcha_stage := current_stage.captcha_stage:
|
||||
captcha_token = attrs.get("captcha_token", None)
|
||||
if not captcha_token:
|
||||
self.stage.logger.warning("Token not set for captcha attempt")
|
||||
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
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.2.2 Blueprint schema",
|
||||
"title": "authentik 2025.2.4 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -6423,8 +6423,6 @@
|
||||
},
|
||||
"acs_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"maxLength": 200,
|
||||
"minLength": 1,
|
||||
"title": "ACS URL"
|
||||
},
|
||||
@ -7887,6 +7885,11 @@
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Sync parent group"
|
||||
},
|
||||
"lookup_groups_from_user": {
|
||||
"type": "boolean",
|
||||
"title": "Lookup groups from user",
|
||||
"description": "Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@ -8233,7 +8236,6 @@
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
@ -8733,8 +8735,6 @@
|
||||
},
|
||||
"sso_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"maxLength": 200,
|
||||
"minLength": 1,
|
||||
"title": "SSO URL",
|
||||
"description": "URL that the initial Login request is sent to."
|
||||
@ -8744,8 +8744,6 @@
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uri",
|
||||
"maxLength": 200,
|
||||
"title": "SLO URL",
|
||||
"description": "Optional URL if your IDP supports Single-Logout."
|
||||
},
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
17
go.mod
17
go.mod
@ -1,12 +1,10 @@
|
||||
module goauthentik.io
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.13.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/getsentry/sentry-go v0.31.1
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
@ -22,17 +20,17 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/sethvargo/go-envconfig v1.1.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025022.6
|
||||
goauthentik.io/api/v3 v3.2025024.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
@ -62,7 +60,6 @@ require (
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
@ -79,6 +76,6 @@ require (
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
31
go.sum
31
go.sum
@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -148,8 +148,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@ -207,8 +208,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@ -239,8 +240,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
@ -299,8 +300,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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025022.6 h1:M5M8Cd/1N7E8KLkvYYh7VdcdKz5nfzjKPFLK+YOtOVg=
|
||||
goauthentik.io/api/v3 v3.2025022.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025024.1 h1:wYmpbNW1XptrjS5dlnZj8CrCs+JUGEVJYStrFdWL9aA=
|
||||
goauthentik.io/api/v3 v3.2025024.1/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -395,8 +396,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -411,8 +412,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -599,8 +600,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
@ -162,13 +162,14 @@ func (c *Config) parseScheme(rawVal string) string {
|
||||
if err != nil {
|
||||
return rawVal
|
||||
}
|
||||
if u.Scheme == "env" {
|
||||
switch u.Scheme {
|
||||
case "env":
|
||||
e, ok := os.LookupEnv(u.Host)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return u.RawQuery
|
||||
} else if u.Scheme == "file" {
|
||||
case "file":
|
||||
d, err := os.ReadFile(u.Path)
|
||||
if err != nil {
|
||||
return u.RawQuery
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigEnv(t *testing.T) {
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar"))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigEnv_Scheme(t *testing.T) {
|
||||
os.Setenv("foo", "bar")
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
|
||||
assert.NoError(t, os.Setenv("foo", "bar"))
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo"))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
@ -33,13 +33,15 @@ func TestConfigEnv_File(t *testing.T) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer func() {
|
||||
assert.NoError(t, os.Remove(file.Name()))
|
||||
}()
|
||||
_, err = file.Write([]byte("bar"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.2"
|
||||
const VERSION = "2025.2.4"
|
||||
|
@ -1,5 +0,0 @@
|
||||
//go:build requirefips
|
||||
|
||||
package backend
|
||||
|
||||
var FipsEnabled = true
|
@ -1,5 +0,0 @@
|
||||
//go:build !requirefips
|
||||
|
||||
package backend
|
||||
|
||||
var FipsEnabled = false
|
@ -35,7 +35,7 @@ func EnableDebugServer() {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
|
||||
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl)
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("failed to write index")
|
||||
return nil
|
||||
|
@ -44,10 +44,11 @@ func New(healthcheck func() bool) *GoUnicorn {
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
if sig == syscall.SIGHUP {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
g.log.Info("SIGHUP received, forwarding to gunicorn")
|
||||
g.Reload()
|
||||
} else if sig == syscall.SIGUSR2 {
|
||||
case syscall.SIGUSR2:
|
||||
g.log.Info("SIGUSR2 received, restarting gunicorn")
|
||||
g.Restart()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package ak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@ -203,7 +204,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
|
||||
"golangVersion": runtime.Version(),
|
||||
"opensslEnabled": cryptobackend.OpensslEnabled,
|
||||
"opensslVersion": cryptobackend.OpensslVersion(),
|
||||
"fipsEnabled": cryptobackend.FipsEnabled,
|
||||
"fipsEnabled": fips140.Enabled(),
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err == nil {
|
||||
|
@ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
req PaginatorRequest[Treq, Tres],
|
||||
opts PaginatorOptions,
|
||||
) ([]Tobj, error) {
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = log.NewEntry(log.StandardLogger())
|
||||
}
|
||||
var bfreq, cfreq interface{}
|
||||
fetchOffset := func(page int32) (Tres, error) {
|
||||
bfreq = req.Page(page)
|
||||
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
||||
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
for {
|
||||
apiObjects, err := fetchOffset(page)
|
||||
if err != nil {
|
||||
if page == 1 {
|
||||
return objects, err
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
@ -1,5 +1,64 @@
|
||||
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) {
|
||||
// req := api.ApiCoreUsersListRequest{}
|
||||
// Paginator(req, PaginatorOptions{
|
||||
|
@ -148,7 +148,8 @@ func (ac *APIController) startWSHandler() {
|
||||
"outpost_type": ac.Server.Type(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
}).Set(1)
|
||||
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
|
||||
switch wsMsg.Instruction {
|
||||
case WebsocketInstructionTriggerUpdate:
|
||||
time.Sleep(ac.reloadOffset)
|
||||
logger.Debug("Got update trigger...")
|
||||
err := ac.OnRefresh()
|
||||
@ -163,7 +164,7 @@ func (ac *APIController) startWSHandler() {
|
||||
"build": constants.BUILD(""),
|
||||
}).SetToCurrentTime()
|
||||
}
|
||||
} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
|
||||
case WebsocketInstructionProviderSpecific:
|
||||
for _, h := range ac.wsHandlers {
|
||||
h(context.Background(), wsMsg.Args)
|
||||
}
|
||||
|
@ -66,7 +66,12 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
||||
return err
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
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")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
|
@ -49,7 +49,12 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
||||
|
@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
|
||||
entries := make([]*ldap.Entry, 0)
|
||||
|
||||
scope := req.SearchRequest.Scope
|
||||
scope := req.Scope
|
||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||
|
||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||
|
@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
|
||||
if !embedded && hostBrowser == "" {
|
||||
return ep
|
||||
}
|
||||
var newHost *url.URL = aku
|
||||
var newHost = aku
|
||||
var newBrowserHost *url.URL
|
||||
if embedded {
|
||||
if authentikHost == "" {
|
||||
|
@ -130,7 +130,12 @@ func (ps *ProxyServer) ServeHTTP() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
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.serve(proxyListener)
|
||||
@ -149,7 +154,12 @@ func (ps *ProxyServer) ServeHTTPS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")
|
||||
|
@ -72,11 +72,13 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
|
||||
session.ID = c.Value
|
||||
|
||||
err = s.load(r.Context(), session)
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else if err == redis.Nil {
|
||||
err = nil // no data stored
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return session, nil
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
session.IsNew = false
|
||||
return session, err
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,12 @@ func (ws *WebServer) listenPlain() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
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.serve(proxyListener)
|
||||
|
@ -46,7 +46,12 @@ func (ws *WebServer) listenTLS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")
|
||||
|
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@ -27,7 +27,7 @@ COPY . .
|
||||
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 \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# 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",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"aws-cdk": "^2.1007.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1005.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
|
||||
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
|
||||
"version": "2.1007.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1007.0.tgz",
|
||||
"integrity": "sha512-/UOYOTGWUm+pP9qxg03tID5tL6euC+pb+xo0RBue+xhnUWwj/Bbsw6DbqbpOPMrNzTUxmM723/uMEQmM6S26dw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"aws-cdk": "^2.1007.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.2.2
|
||||
Default: 2025.2.4
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -1220,6 +1220,20 @@ msgstr ""
|
||||
msgid "Reputation Scores"
|
||||
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
|
||||
msgid "Permission denied"
|
||||
msgstr ""
|
||||
|
@ -10,8 +10,8 @@
|
||||
# Manuel Viens, 2023
|
||||
# Mordecai, 2023
|
||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||
# Tina, 2024
|
||||
# Charles Leclerc, 2025
|
||||
# Tina, 2025
|
||||
# Marc Schmitt, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -1347,6 +1347,22 @@ msgstr "Score de Réputation"
|
||||
msgid "Reputation Scores"
|
||||
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
|
||||
msgid "Permission denied"
|
||||
msgstr "Permission refusée"
|
||||
|
@ -6,23 +6,23 @@
|
||||
# Translators:
|
||||
# Dario Rigolin, 2022
|
||||
# aoor9, 2023
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2024
|
||||
# Enrico Campani, 2024
|
||||
# Marco Vitale, 2024
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
|
||||
# Nicola Mersi, 2024
|
||||
# tmassimi, 2024
|
||||
# Marc Schmitt, 2024
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2025
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-14 14:49+0000\n"
|
||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2024\n"
|
||||
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -130,6 +130,10 @@ msgstr "L'utente non ha accesso all'applicazione."
|
||||
msgid "Extra description not available"
|
||||
msgstr "Descrizione extra non disponibile"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "Impossibile impostare il gruppo come padre di se stesso."
|
||||
|
||||
#: authentik/core/api/providers.py
|
||||
msgid ""
|
||||
"When not set all providers are returned. When set to true, only backchannel "
|
||||
@ -177,6 +181,14 @@ msgstr "Aggiungi utente al gruppo"
|
||||
msgid "Remove user from group"
|
||||
msgstr "Rimuovi l'utente dal gruppo"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Enable superuser status"
|
||||
msgstr "Abilita stato di superutente"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Disable superuser status"
|
||||
msgstr "Disabilita stato di superutente"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "User's display name."
|
||||
msgstr "Nome visualizzato dell'utente."
|
||||
@ -260,11 +272,11 @@ msgstr "Applicazioni"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlement"
|
||||
msgstr ""
|
||||
msgstr "Entitlement Applicazione"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlements"
|
||||
msgstr ""
|
||||
msgstr "Entitlements Applicazione"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the source-specific identifier"
|
||||
@ -551,62 +563,6 @@ msgstr "Mappatura Microsoft Entra Provider"
|
||||
msgid "Microsoft Entra Provider Mappings"
|
||||
msgstr "Mappature Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"Determines how long a session lasts. Default of 0 means that the sessions "
|
||||
"lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
"Determina quanto può durare una sessione. Se impostato a 0, la sessione "
|
||||
"durerà fino alla chiusura del browser. (Formato: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "When set to true, connection tokens will be deleted upon disconnect."
|
||||
msgstr ""
|
||||
"Se impostato su vero, i token di connessione verranno eliminati alla "
|
||||
"disconnessione."
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Provider"
|
||||
msgstr "Fornitore di controllo dell'accesso remoto"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Providers"
|
||||
msgstr "Fornitori di controllo dell'accesso remoto"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Endpoint"
|
||||
msgstr "Endpoint RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Endpoints"
|
||||
msgstr "Endpoints RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Provider Property Mapping"
|
||||
msgstr "Mappatura delle proprietà del provider RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Provider Property Mappings"
|
||||
msgstr "Mappature proprietà del provider RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Connection token"
|
||||
msgstr "RAC Connection token"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Connection tokens"
|
||||
msgstr "RAC Connection tokens"
|
||||
|
||||
#: authentik/enterprise/providers/rac/views.py
|
||||
msgid "Maximum connection limit reached."
|
||||
msgstr "Limite massimo di connessioni raggiunto."
|
||||
|
||||
#: authentik/enterprise/providers/rac/views.py
|
||||
msgid "(You are already connected in another tab/window)"
|
||||
msgstr "(Sei già connesso in un'altra scheda/finestra)"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Signing Key"
|
||||
@ -614,39 +570,39 @@ msgstr "Chiave di firma"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Key used to sign the SSF Events."
|
||||
msgstr ""
|
||||
msgstr "Chiave utilizzata per firmare gli eventi SSF."
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Provider"
|
||||
msgstr ""
|
||||
msgstr "Fornitore Shared Signals Framework"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Providers"
|
||||
msgstr ""
|
||||
msgstr "Fornitori Shared Signals Framework"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Add stream to SSF provider"
|
||||
msgstr ""
|
||||
msgstr "Aggiungi Stream al provider SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream"
|
||||
msgstr ""
|
||||
msgstr "SSF Stream"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Streams"
|
||||
msgstr ""
|
||||
msgstr "SSF Streams"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Event"
|
||||
msgstr ""
|
||||
msgstr "Evento di Stream SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Events"
|
||||
msgstr ""
|
||||
msgstr "Eventi di Stream SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr ""
|
||||
msgstr "Impossibile inviare la richiesta"
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@ -712,9 +668,26 @@ msgid "Slack Webhook (Slack/Discord)"
|
||||
msgstr "Slack Webhook (Slack/Discord)"
|
||||
|
||||
#: authentik/events/models.py
|
||||
#: authentik/stages/authenticator_validate/models.py
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr ""
|
||||
"Personalizza il corpo della richiesta. Il mapping dovrebbe restituire dati "
|
||||
"serializzabili in JSON."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr ""
|
||||
"Configurare le intestazioni aggiuntive da inviare. Il mapping dovrebbe "
|
||||
"restituire un dizionario di coppie chiave-valore."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
@ -944,7 +917,7 @@ msgstr ""
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid "Evaluate policies when the Stage is presented to the user."
|
||||
msgstr ""
|
||||
msgstr "Valutare i criteri quando la fase viene presentata all'utente."
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid ""
|
||||
@ -986,6 +959,14 @@ msgstr "Tokens del flusso"
|
||||
msgid "Invalid next URL"
|
||||
msgstr "URL successivo non valido"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr ""
|
||||
"Quando abilitato, il provider non modificherà o creerà oggetti nel sistema "
|
||||
"remoto."
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr "Avvio della sincronizzazione completa del provider"
|
||||
@ -1000,6 +981,10 @@ msgstr "Sincronizzando pagina {page} degli utenti"
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Sincronizzando pagina {page} dei gruppi"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr "Richiesta di mutazione ignorata a causa della prova di funzionamento"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
@ -1212,6 +1197,14 @@ msgstr "GeoIP: indirizzo IP del client non trovato nel database della città."
|
||||
msgid "Client IP is not in an allowed country."
|
||||
msgstr "L'IP del client non si trova in un paese consentito."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Distance from previous authentication is larger than threshold."
|
||||
msgstr "La distanza dall'autenticazione precedente è maggiore della soglia."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Distance is further than possible."
|
||||
msgstr "La distanza è maggiore del possibile."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "GeoIP Policy"
|
||||
msgstr "Criterio GeoIP"
|
||||
@ -1344,6 +1337,22 @@ msgstr "Punteggio di reputazione"
|
||||
msgid "Reputation Scores"
|
||||
msgstr "Punteggi di reputazione"
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid "Waiting for authentication..."
|
||||
msgstr "In attesa di autenticazione..."
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid ""
|
||||
"You're already authenticating in another tab. This page will refresh once "
|
||||
"authentication is completed."
|
||||
msgstr ""
|
||||
"Ti stai già autenticando in un'altra scheda. Questa pagina si aggiornerà una"
|
||||
" volta completata l'autenticazione."
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid "Authenticate in this tab"
|
||||
msgstr "Autenticati in questa scheda"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Permission denied"
|
||||
msgstr "Permesso negato"
|
||||
@ -1531,6 +1540,14 @@ msgstr "RS256 (Crittografia Asimmetrica)"
|
||||
msgid "ES256 (Asymmetric Encryption)"
|
||||
msgstr "ES256 (Crittografia Asimmetrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr "ES384 (Crittografia Asimmetrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr "ES512 (Crittografia Asimmetrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
msgstr "Scope usato dall'utente"
|
||||
@ -1814,6 +1831,61 @@ msgstr "Provider Proxy"
|
||||
msgid "Proxy Providers"
|
||||
msgstr "Providers Proxy"
|
||||
|
||||
#: authentik/providers/rac/models.py authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"Determines how long a session lasts. Default of 0 means that the sessions "
|
||||
"lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
"Determina quanto può durare una sessione. Se impostato a 0, la sessione "
|
||||
"durerà fino alla chiusura del browser. (Formato: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "When set to true, connection tokens will be deleted upon disconnect."
|
||||
msgstr ""
|
||||
"Se impostato su vero, i token di connessione verranno eliminati alla "
|
||||
"disconnessione."
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Provider"
|
||||
msgstr "Fornitore di controllo dell'accesso remoto"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Providers"
|
||||
msgstr "Fornitori di controllo dell'accesso remoto"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Endpoint"
|
||||
msgstr "Endpoint RAC"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Endpoints"
|
||||
msgstr "Endpoints RAC"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Provider Property Mapping"
|
||||
msgstr "Mappatura delle proprietà del provider RAC"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Provider Property Mappings"
|
||||
msgstr "Mappature proprietà del provider RAC"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Connection token"
|
||||
msgstr "RAC Connection token"
|
||||
|
||||
#: authentik/providers/rac/models.py
|
||||
msgid "RAC Connection tokens"
|
||||
msgstr "RAC Connection tokens"
|
||||
|
||||
#: authentik/providers/rac/views.py
|
||||
msgid "Maximum connection limit reached."
|
||||
msgstr "Limite massimo di connessioni raggiunto."
|
||||
|
||||
#: authentik/providers/rac/views.py
|
||||
msgid "(You are already connected in another tab/window)"
|
||||
msgstr "(Sei già connesso in un'altra scheda/finestra)"
|
||||
|
||||
#: authentik/providers/radius/models.py
|
||||
msgid "Shared secret between clients and server to hash packets."
|
||||
msgstr "Segreto condiviso tra client e server per hashare i pacchetti."
|
||||
@ -1901,6 +1973,20 @@ msgstr ""
|
||||
"Configura il modo in cui verrà creato il valore NameID. Se lasciato vuoto, "
|
||||
"verrà considerato il NameIDPolicy della richiesta in arrivo"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr "Mapping delle proprietà 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 ""
|
||||
"Configura come verrà creato il valore AuthnContextClassRef. Se lasciato "
|
||||
"vuoto, AuthnContextClassRef verrà impostato in base ai metodi di "
|
||||
"autenticazione utilizzati dall'utente."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: "
|
||||
@ -2042,6 +2128,18 @@ msgstr "Provider SAML dai Metadati"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "Providers SAML dai Metadati"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "Predefinito"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "URL di base per le richieste SCIM, di solito termina con /v2"
|
||||
@ -2050,6 +2148,16 @@ msgstr "URL di base per le richieste SCIM, di solito termina con /v2"
|
||||
msgid "Authentication token"
|
||||
msgstr "Token di autenticazione"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "SCIM Modalità di Compatibilità"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
"Modifica il comportamento di autenticazione per le implementazioni SCIM "
|
||||
"specifiche del fornitore."
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "Privider SCIM"
|
||||
@ -2125,7 +2233,7 @@ msgstr ""
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr ""
|
||||
msgstr "Tipo server KAdmin"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
@ -2730,6 +2838,117 @@ msgstr "Dispositivo Duo"
|
||||
msgid "Duo Devices"
|
||||
msgstr "Dispositivi Duo"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email OTP"
|
||||
msgstr "Email OTP"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid ""
|
||||
"When enabled, global Email connection settings will be used and connection "
|
||||
"settings below will be ignored."
|
||||
msgstr ""
|
||||
"Se abilitato, verranno utilizzate le impostazioni di connessione e-mail "
|
||||
"globali e le impostazioni di connessione riportate di seguito verranno "
|
||||
"ignorate."
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr ""
|
||||
"Tempo di validità del token inviato (formato: "
|
||||
"hours=3,minutes=17,seconds=300)."
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Authenticator Setup Stage"
|
||||
msgstr "Fase di configurazione dell'autenticatore email"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Authenticator Setup Stages"
|
||||
msgstr "Fasi di configurazione dell'autenticatore email"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Exception occurred while rendering E-mail template"
|
||||
msgstr ""
|
||||
"Eccezione verificatasi durante la visualizzazione del modello di posta "
|
||||
"elettronica"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Device"
|
||||
msgstr "Dispositivo email"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Devices"
|
||||
msgstr "Dispositivi email"
|
||||
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
#: authentik/stages/authenticator_sms/stage.py
|
||||
#: authentik/stages/authenticator_totp/stage.py
|
||||
msgid "Code does not match"
|
||||
msgstr "Il codice non corrisponde"
|
||||
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
msgid "Invalid email"
|
||||
msgstr "Email non valida"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
#: authentik/stages/email/templates/email/password_reset.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Hi %(username)s,\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Ciao %(username)s,\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
msgid ""
|
||||
"\n"
|
||||
" Email MFA code.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Codice MFA via e-mail.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request this code, please ignore this email. The code above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Se non hai richiesto questo codice, ignora questa email. Il codice sopra riportato è valido per %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
#, python-format
|
||||
msgid "Hi %(username)s,"
|
||||
msgstr "Ciao %(username)s,"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
msgid ""
|
||||
"\n"
|
||||
"Email MFA code\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Codice e-mail MFA\n"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request this code, please ignore this email. The code above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Se non hai richiesto questo codice, ignora questa email. Il codice sopra riportato è valido per %(expires)s.\n"
|
||||
|
||||
#: authentik/stages/authenticator_sms/models.py
|
||||
msgid ""
|
||||
"When enabled, the Phone number is only used during enrollment to verify the "
|
||||
@ -2768,11 +2987,6 @@ msgstr "Dispositivo SMS"
|
||||
msgid "SMS Devices"
|
||||
msgstr "Dispositivi SMS"
|
||||
|
||||
#: authentik/stages/authenticator_sms/stage.py
|
||||
#: authentik/stages/authenticator_totp/stage.py
|
||||
msgid "Code does not match"
|
||||
msgstr "Il codice non corrisponde"
|
||||
|
||||
#: authentik/stages/authenticator_sms/stage.py
|
||||
msgid "Invalid phone number"
|
||||
msgstr "Numero di telefono non valido"
|
||||
@ -3013,23 +3227,10 @@ msgstr "Ripristino password"
|
||||
msgid "Account Confirmation"
|
||||
msgstr "Conferma dell'account"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid ""
|
||||
"When enabled, global Email connection settings will be used and connection "
|
||||
"settings below will be ignored."
|
||||
msgstr ""
|
||||
"Se abilitato, verranno utilizzate le impostazioni di connessione e-mail "
|
||||
"globali e le impostazioni di connessione riportate di seguito verranno "
|
||||
"ignorate."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "Attiva gli utenti al completamento della fase."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "Tempo in minuti in cui il token inviato è valido."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "Fase email"
|
||||
@ -3038,12 +3239,6 @@ msgstr "Fase email"
|
||||
msgid "Email Stages"
|
||||
msgstr "Fasi Email"
|
||||
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Exception occurred while rendering E-mail template"
|
||||
msgstr ""
|
||||
"Eccezione verificatasi durante la visualizzazione del modello di posta "
|
||||
"elettronica"
|
||||
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Successfully verified Email."
|
||||
msgstr "Email verificato con successo."
|
||||
@ -3127,17 +3322,6 @@ msgstr ""
|
||||
"\n"
|
||||
"Questa email è stata inviata dal trasporto delle notifiche %(name)s.\n"
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Hi %(username)s,\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Ciao %(username)s,\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.html
|
||||
msgid ""
|
||||
"\n"
|
||||
@ -3158,11 +3342,6 @@ msgstr ""
|
||||
" Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
#, python-format
|
||||
msgid "Hi %(username)s,"
|
||||
msgstr "Ciao %(username)s,"
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
msgid ""
|
||||
"\n"
|
||||
@ -3492,6 +3671,7 @@ msgstr ""
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr ""
|
||||
"Il flusso target dovrebbe essere presente quando la modalità è Flusso."
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -1234,6 +1234,20 @@ msgstr "信誉分数"
|
||||
msgid "Reputation Scores"
|
||||
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
|
||||
msgid "Permission denied"
|
||||
msgstr "权限被拒绝"
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.2",
|
||||
"version": "2025.2.4",
|
||||
"private": true
|
||||
}
|
||||
|
11
packages/eslint-config/@types/eslint-plugin-react-hooks.d.ts
vendored
Normal file
11
packages/eslint-config/@types/eslint-plugin-react-hooks.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @file TypeScript type definitions for eslint-plugin-react-hooks
|
||||
*/
|
||||
declare module "eslint-plugin-react-hooks" {
|
||||
import { ESLint } from "eslint";
|
||||
// We have to do this because ESLint aliases the namespace and class simultaneously.
|
||||
type PluginInstance = ESLint.Plugin;
|
||||
const Plugin: PluginInstance;
|
||||
|
||||
export default Plugin;
|
||||
}
|
11
packages/eslint-config/@types/eslint-plugin-react.d.ts
vendored
Normal file
11
packages/eslint-config/@types/eslint-plugin-react.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @file TypeScript type definitions for eslint-plugin-react
|
||||
*/
|
||||
declare module "eslint-plugin-react" {
|
||||
import { ESLint } from "eslint";
|
||||
// We have to do this because ESLint aliases the namespace and class simultaneously.
|
||||
type PluginInstance = ESLint.Plugin;
|
||||
const Plugin: PluginInstance;
|
||||
|
||||
export default Plugin;
|
||||
}
|
18
packages/eslint-config/LICENSE.txt
Normal file
18
packages/eslint-config/LICENSE.txt
Normal file
@ -0,0 +1,18 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 Authentik Security, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
5
packages/eslint-config/README.md
Normal file
5
packages/eslint-config/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# `@goauthentik/eslint-config`
|
||||
|
||||
This package contains the ESLint configuration used by authentik.
|
||||
While it is possible to use this configuration outside of our projects,
|
||||
you may find that it is not as useful as other popular configurations.
|
72
packages/eslint-config/index.js
Normal file
72
packages/eslint-config/index.js
Normal file
@ -0,0 +1,72 @@
|
||||
import eslint from "@eslint/js";
|
||||
import { javaScriptConfig } from "@goauthentik/eslint-config/javascript-config";
|
||||
import { reactConfig } from "@goauthentik/eslint-config/react-config";
|
||||
import { typescriptConfig } from "@goauthentik/eslint-config/typescript-config";
|
||||
import * as litconf from "eslint-plugin-lit";
|
||||
import * as wcconf from "eslint-plugin-wc";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef ESLintPackageConfigOptions Options for creating package ESLint configuration.
|
||||
* @property {string[]} [ignorePatterns] Override ignore patterns for ESLint.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {string[]} Default Ignore patterns for ESLint.
|
||||
*/
|
||||
export const DefaultIgnorePatterns = [
|
||||
// ---
|
||||
"**/*.md",
|
||||
"**/out",
|
||||
"**/dist",
|
||||
"**/.wireit",
|
||||
"website/build/**",
|
||||
"website/.docusaurus/**",
|
||||
"**/node_modules",
|
||||
"**/coverage",
|
||||
"**/storybook-static",
|
||||
"**/locale-codes.ts",
|
||||
"**/src/locales",
|
||||
"**/gen-ts-api",
|
||||
];
|
||||
|
||||
/**
|
||||
* Given a preferred package name, creates a ESLint configuration object.
|
||||
*
|
||||
* @param {ESLintPackageConfigOptions} options The preferred package configuration options.
|
||||
*
|
||||
* @returns The ESLint configuration object.
|
||||
*/
|
||||
export function createESLintPackageConfig({ ignorePatterns = DefaultIgnorePatterns } = {}) {
|
||||
return tseslint.config(
|
||||
{
|
||||
ignores: ignorePatterns,
|
||||
},
|
||||
|
||||
eslint.configs.recommended,
|
||||
javaScriptConfig,
|
||||
|
||||
wcconf.configs["flat/recommended"],
|
||||
litconf.configs["flat/recommended"],
|
||||
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
...typescriptConfig,
|
||||
|
||||
...reactConfig,
|
||||
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
files: [
|
||||
// ---
|
||||
"**/scripts/**/*",
|
||||
"**/test/**/*",
|
||||
"**/tests/**/*",
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
143
packages/eslint-config/javascript-config.js
Normal file
143
packages/eslint-config/javascript-config.js
Normal file
@ -0,0 +1,143 @@
|
||||
// @ts-check
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const MAX_DEPTH = 4;
|
||||
const MAX_NESTED_CALLBACKS = 4;
|
||||
const MAX_PARAMS = 5;
|
||||
|
||||
/**
|
||||
* ESLint configuration for JavaScript authentik projects.
|
||||
*/
|
||||
export const javaScriptConfig = tseslint.config({
|
||||
rules: {
|
||||
// TODO: Clean up before enabling.
|
||||
"accessor-pairs": "off",
|
||||
"array-callback-return": "error",
|
||||
"block-scoped-var": "error",
|
||||
"consistent-return": ["error", { treatUndefinedAsUnspecified: false }],
|
||||
"consistent-this": ["error", "that"],
|
||||
"curly": "off",
|
||||
"dot-notation": [
|
||||
"error",
|
||||
{
|
||||
allowKeywords: true,
|
||||
},
|
||||
],
|
||||
"eqeqeq": "error",
|
||||
"func-names": ["error", "as-needed"],
|
||||
"guard-for-in": "error",
|
||||
"max-depth": ["error", MAX_DEPTH],
|
||||
"max-nested-callbacks": ["error", MAX_NESTED_CALLBACKS],
|
||||
"max-params": ["error", MAX_PARAMS],
|
||||
// TODO: Clean up before enabling.
|
||||
// "new-cap": "error",
|
||||
"no-alert": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-bitwise": [
|
||||
"error",
|
||||
{
|
||||
allow: ["~"],
|
||||
int32Hint: true,
|
||||
},
|
||||
],
|
||||
"no-caller": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-div-regex": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-else-return": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-function": ["error", { allow: ["constructors"] }],
|
||||
"no-labels": "error",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implicit-globals": "error",
|
||||
"no-inner-declarations": ["error", "functions"],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-loop-func": "error",
|
||||
"no-multi-str": "error",
|
||||
// TODO: Clean up before enabling.
|
||||
"no-negated-condition": "off",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": ["error", { props: false }],
|
||||
"no-proto": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-restricted-syntax": ["error", "WithStatement"],
|
||||
"no-script-url": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
// TODO: Clean up before enabling.
|
||||
// "no-shadow": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "off", // Handled by Prettier.
|
||||
"no-undef": "off",
|
||||
"no-undef-init": "off",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-use-before-define": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-var": "error",
|
||||
"no-void": "error",
|
||||
"no-with": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "error",
|
||||
"radix": "error",
|
||||
"require-yield": "error",
|
||||
"strict": ["error", "global"],
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"vars-on-top": "error",
|
||||
"yoda": ["error", "never"],
|
||||
|
||||
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
||||
// SonarJS is not yet compatible with ESLint 9. Commenting these out
|
||||
// until it is.
|
||||
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
|
||||
// "sonarjs/no-duplicate-string": "off",
|
||||
// "sonarjs/no-nested-template-literals": "off",
|
||||
},
|
||||
});
|
||||
|
||||
export default javaScriptConfig;
|
53
packages/eslint-config/package.json
Normal file
53
packages/eslint-config/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@goauthentik/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"description": "authentik's ESLint config",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": "./index.js",
|
||||
"types": "./out/index.d.ts"
|
||||
},
|
||||
"./react-config": {
|
||||
"import": "./react-config.js",
|
||||
"types": "./out/react-config.d.ts"
|
||||
},
|
||||
"./javascript-config": {
|
||||
"import": "./javascript-config.js",
|
||||
"types": "./out/javascript-config.d.ts"
|
||||
},
|
||||
"./typescript-config": {
|
||||
"import": "./typescript-config.js",
|
||||
"types": "./out/typescript-config.d.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@goauthentik/tsconfig": "1.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.29.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.29.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
"types": "./out/index.d.ts",
|
||||
"prettier": "@goauthentik/prettier-config",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
34
packages/eslint-config/react-config.js
vendored
Normal file
34
packages/eslint-config/react-config.js
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import hooksPlugin from "eslint-plugin-react-hooks";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
/**
|
||||
* ESLint configuration for React authentik projects.
|
||||
*/
|
||||
export const reactConfig = tseslint.config({
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
"react": reactPlugin,
|
||||
"react-hooks": hooksPlugin,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
"react/jsx-uses-react": 0,
|
||||
|
||||
"react/display-name": "off",
|
||||
"react/jsx-curly-brace-presence": "error",
|
||||
"react/jsx-no-leaked-render": "error",
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
});
|
||||
|
||||
export default reactConfig;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user