Compare commits
6 Commits
static-con
...
typescript
| Author | SHA1 | Date | |
|---|---|---|---|
| ff3e59c8e2 | |||
| 0e57e06191 | |||
| 40ab2bccfd | |||
| efc0697483 | |||
| 9c150d74c4 | |||
| d421b25760 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.4
|
||||
current_version = 2025.2.2
|
||||
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,8 +17,6 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:pyproject.toml]
|
||||
|
||||
[bumpversion:file:uv.lock]
|
||||
|
||||
[bumpversion:file:package.json]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
@ -10,6 +10,9 @@ insert_final_newline = true
|
||||
[*.html]
|
||||
indent_size = 2
|
||||
|
||||
[schemas/*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Documentation issue
|
||||
about: Suggest an improvement or report a problem
|
||||
title: ""
|
||||
labels: documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
||||
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
||||
|
||||
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
||||
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the documentation issue here.
|
||||
|
||||
**Consider opening a PR!**
|
||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
||||
@ -44,6 +44,7 @@ if is_release:
|
||||
]
|
||||
if not prerelease:
|
||||
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@v7
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 5000s --verbose
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@ -213,3 +213,25 @@ source_docs/
|
||||
|
||||
### Docker ###
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules/
|
||||
|
||||
|
||||
# Wireit's cache
|
||||
.wireit
|
||||
|
||||
custom-elements.json
|
||||
|
||||
|
||||
### Development ###
|
||||
.drafts
|
||||
|
||||
47
.prettierignore
Normal file
47
.prettierignore
Normal file
@ -0,0 +1,47 @@
|
||||
# Prettier Ignorefile
|
||||
|
||||
## Static Files
|
||||
**/LICENSE
|
||||
|
||||
authentik/stages/**/*
|
||||
|
||||
## Build asset directories
|
||||
coverage
|
||||
dist
|
||||
out
|
||||
.docusaurus
|
||||
website/docs/developer-docs/api/**/*
|
||||
|
||||
## Environment
|
||||
*.env
|
||||
|
||||
## Secrets
|
||||
*.secrets
|
||||
|
||||
## Yarn
|
||||
.yarn/**/*
|
||||
|
||||
## Node
|
||||
node_modules
|
||||
coverage
|
||||
|
||||
## Configs
|
||||
*.log
|
||||
*.yaml
|
||||
*.yml
|
||||
|
||||
# Templates
|
||||
# TODO: Rename affected files to *.template.* or similar.
|
||||
*.html
|
||||
*.mdx
|
||||
*.md
|
||||
|
||||
## Import order matters
|
||||
poly.ts
|
||||
src/locale-codes.ts
|
||||
src/locales/
|
||||
|
||||
# Storybook
|
||||
storybook-static/
|
||||
.storybook/css-import-maps*
|
||||
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -17,6 +17,6 @@
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
||||
|
||||
57
.vscode/settings.json
vendored
57
.vscode/settings.json
vendored
@ -16,7 +16,7 @@
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||
"typescript.tsdk": "./web/node_modules/typescript/lib",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"yaml.schemas": {
|
||||
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
||||
@ -30,7 +30,56 @@
|
||||
}
|
||||
],
|
||||
"go.testFlags": ["-count=1"],
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
|
||||
|
||||
"eslint.useFlatConfig": true,
|
||||
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.cjs": "*.d.cts",
|
||||
"package.json": "package-lock.json, yarn.lock, .yarnrc, .yarnrc.yml, .yarn, .nvmrc, .node-version",
|
||||
"tsconfig.json": "tsconfig.*.json, jsconfig.json"
|
||||
},
|
||||
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/*.code-search": true,
|
||||
"**/dist": true,
|
||||
"**/out": true,
|
||||
"**/package-lock.json": true
|
||||
},
|
||||
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.removeUnusedImports": "explicit"
|
||||
},
|
||||
// We use Prettier for formatting, but specifying these settings
|
||||
// will ensure that VS Code's IntelliSense doesn't autocomplete unformatted code.
|
||||
"javascript.format.semicolons": "insert",
|
||||
"typescript.format.semicolons": "insert",
|
||||
"javascript.preferences.quoteStyle": "double",
|
||||
"typescript.preferences.quoteStyle": "double"
|
||||
}
|
||||
|
||||
40
.vscode/tasks.json
vendored
40
.vscode/tasks.json
vendored
@ -4,12 +4,7 @@
|
||||
{
|
||||
"label": "authentik/core: make",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"lint-fix",
|
||||
"lint"
|
||||
],
|
||||
"args": ["run", "make", "lint-fix", "lint"],
|
||||
"presentation": {
|
||||
"panel": "new"
|
||||
},
|
||||
@ -18,11 +13,7 @@
|
||||
{
|
||||
"label": "authentik/core: run",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"ak",
|
||||
"server"
|
||||
],
|
||||
"args": ["run", "ak", "server"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -32,17 +23,13 @@
|
||||
{
|
||||
"label": "authentik/web: make",
|
||||
"command": "make",
|
||||
"args": [
|
||||
"web"
|
||||
],
|
||||
"args": ["web"],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/web: watch",
|
||||
"command": "make",
|
||||
"args": [
|
||||
"web-watch"
|
||||
],
|
||||
"args": ["web-watch"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -52,26 +39,19 @@
|
||||
{
|
||||
"label": "authentik: install",
|
||||
"command": "make",
|
||||
"args": [
|
||||
"install",
|
||||
"-j4"
|
||||
],
|
||||
"args": ["install", "-j4"],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/website: make",
|
||||
"command": "make",
|
||||
"args": [
|
||||
"website"
|
||||
],
|
||||
"args": ["website"],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/website: watch",
|
||||
"command": "make",
|
||||
"args": [
|
||||
"website-watch"
|
||||
],
|
||||
"args": ["website-watch"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -81,11 +61,7 @@
|
||||
{
|
||||
"label": "authentik/api: generate",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"gen"
|
||||
],
|
||||
"args": ["run", "make", "gen"],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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} docker.io/library/golang:1.24-bookworm AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG 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 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" 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.14 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.9 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
|
||||
12
Makefile
12
Makefile
@ -133,16 +133,14 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/${GEN_API_TS} \
|
||||
-c /local/scripts/api-ts-config.yaml \
|
||||
--input-spec /local/schema.yml \
|
||||
--generator-name typescript-fetch \
|
||||
--output /local/${GEN_API_TS} \
|
||||
--config /local/scripts/api-ts-config.yaml \
|
||||
--additional-properties=npmVersion=${NPM_VERSION} \
|
||||
--git-repo-id authentik \
|
||||
--git-user-id goauthentik
|
||||
mkdir -p web/node_modules/@goauthentik/api
|
||||
cd ./${GEN_API_TS} && npm i
|
||||
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
|
||||
npm install
|
||||
|
||||
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
docker run \
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.4"
|
||||
__version__ = "2025.2.2"
|
||||
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,13 +179,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"user",
|
||||
"source",
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -202,7 +199,7 @@ class UserSourceConnectionViewSet(
|
||||
queryset = UserSourceConnection.objects.all()
|
||||
serializer_class = UserSourceConnectionSerializer
|
||||
filterset_fields = ["user", "source__slug"]
|
||||
search_fields = ["user__username", "source__slug", "identifier"]
|
||||
search_fields = ["source__slug"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
||||
@ -221,11 +218,9 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -242,5 +237,6 @@ class GroupSourceConnectionViewSet(
|
||||
queryset = GroupSourceConnection.objects.all()
|
||||
serializer_class = GroupSourceConnectionSerializer
|
||||
filterset_fields = ["group", "source__slug"]
|
||||
search_fields = ["group__name", "source__slug", "identifier"]
|
||||
search_fields = ["source__slug"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
"""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.base import SessionBase
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
class UserGroupSerializer(ModelSerializer):
|
||||
@ -228,7 +226,6 @@ class UserSerializer(ModelSerializer):
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"groups_obj",
|
||||
@ -243,7 +240,6 @@ class UserSerializer(ModelSerializer):
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"allow_blank": True},
|
||||
"date_joined": {"read_only": True},
|
||||
"password_change_date": {"read_only": True},
|
||||
}
|
||||
|
||||
@ -377,7 +373,7 @@ class UsersFilter(FilterSet):
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(field_name="path")
|
||||
@ -395,11 +391,6 @@ 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:
|
||||
@ -778,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
if not instance.is_active:
|
||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||
session_ids = sessions.values_list("session_key", flat=True)
|
||||
for session in session_ids:
|
||||
SessionStore(session).delete()
|
||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||
sessions.delete()
|
||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||
return response
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@ -1,30 +0,0 @@
|
||||
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,17 +761,11 @@ 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:
|
||||
@ -786,14 +780,10 @@ 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):
|
||||
@ -824,7 +814,6 @@ 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()
|
||||
|
||||
@ -838,10 +827,6 @@ 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,10 +1,7 @@
|
||||
"""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.base import SessionBase
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
@ -28,7 +25,6 @@ password_changed = Signal()
|
||||
login_failed = Signal()
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@receiver(post_save, sender=Application)
|
||||
@ -64,7 +60,8 @@ 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"""
|
||||
SessionStore(instance.session_key).delete()
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
|
||||
@ -48,7 +48,6 @@ 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
|
||||
|
||||
|
||||
@ -262,7 +261,6 @@ 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(
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
from django.apps import apps
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestSourceAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_builtin_source_used_by(self):
|
||||
"""Test Providers's types endpoint"""
|
||||
apps.get_app_config("authentik_core").source_inbuilt()
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -1,7 +1,6 @@
|
||||
"""Test Users API"""
|
||||
|
||||
from datetime import datetime
|
||||
from json import loads
|
||||
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
@ -16,12 +15,7 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import (
|
||||
create_test_admin_user,
|
||||
create_test_brand,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = create_test_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
|
||||
def test_filter_type(self):
|
||||
"""Test API filtering by type"""
|
||||
@ -47,35 +41,6 @@ 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)
|
||||
@ -134,8 +99,6 @@ 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,11 +13,7 @@ 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 (
|
||||
GroupSourceConnectionViewSet,
|
||||
SourceViewSet,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||
from authentik.core.api.users import UserViewSet
|
||||
@ -85,7 +81,6 @@ 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,14 +11,13 @@ 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_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.stage import ChallengeStageView, StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
@ -54,9 +53,6 @@ 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,7 +69,6 @@ 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"
|
||||
|
||||
@ -454,7 +453,6 @@ 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,22 +6,14 @@ 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, FlowDesignation
|
||||
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
|
||||
from authentik.flows.models import Flow
|
||||
|
||||
|
||||
class FlowInterfaceView(InterfaceView):
|
||||
"""Flow interface"""
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
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["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
kwargs["inspector"] = "inspector" in self.request.GET
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@ -1,20 +1,5 @@
|
||||
# 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.
|
||||
|
||||
# update website/docs/install-config/configuration/configuration.mdx
|
||||
# This is the default configuration file
|
||||
postgresql:
|
||||
host: localhost
|
||||
name: authentik
|
||||
|
||||
@ -18,15 +18,6 @@ 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,7 +13,6 @@ 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
|
||||
@ -185,7 +184,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
self.client.images.pull(image)
|
||||
except DockerException: # pragma: no cover
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
|
||||
self.client.images.pull(image)
|
||||
return image
|
||||
|
||||
|
||||
@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
let redirecting = false;
|
||||
const checkAuth = async () => {
|
||||
if (redirecting) return true;
|
||||
const url = "{{ check_auth_url }}";
|
||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
||||
try {
|
||||
const result = await fetch(url, {
|
||||
method: "HEAD",
|
||||
});
|
||||
if (result.status >= 400) {
|
||||
return false
|
||||
}
|
||||
console.debug("authentik/policies/buffer: Continuing");
|
||||
redirecting = true;
|
||||
if ("{{ auth_req_method }}" === "post") {
|
||||
document.querySelector("form").submit();
|
||||
} else {
|
||||
window.location.assign("{{ continue_url|escapejs }}");
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let timeout = 100;
|
||||
let offset = 20;
|
||||
let attempt = 0;
|
||||
const main = async () => {
|
||||
attempt += 1;
|
||||
await checkAuth();
|
||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
||||
setTimeout(main, timeout);
|
||||
timeout += (offset * attempt);
|
||||
if (timeout >= 2000) {
|
||||
timeout = 2000;
|
||||
}
|
||||
}
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
||||
await checkAuth();
|
||||
});
|
||||
main();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Waiting for authentication...' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
||||
{% if auth_req_method == "post" %}
|
||||
{% for key, value in auth_req_body.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<div class="pf-c-empty-state__icon">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans "Authenticate in this tab" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -1,121 +0,0 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application, Provider
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.views import (
|
||||
QS_BUFFER_ID,
|
||||
SESSION_KEY_BUFFER,
|
||||
BufferedPolicyAccessView,
|
||||
BufferView,
|
||||
PolicyAccessView,
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyViews(TestCase):
|
||||
"""Test PolicyAccessView"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_test_user()
|
||||
|
||||
def test_pav(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
|
||||
class TestView(PolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = self.user
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, b"foo")
|
||||
|
||||
def test_pav_buffer(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
||||
|
||||
def test_pav_buffer_skip(self):
|
||||
"""Test simple policy access view (skip buffer)"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/?skip_buffer=true")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test buffer view"""
|
||||
uid = generate_id()
|
||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
ts = generate_id()
|
||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
||||
"method": "get",
|
||||
"body": {},
|
||||
"url": f"/{ts}",
|
||||
}
|
||||
req.session.save()
|
||||
|
||||
res = BufferView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn(ts, res.render().content.decode())
|
||||
@ -1,14 +1,7 @@
|
||||
"""API URLs"""
|
||||
|
||||
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,37 +1,23 @@
|
||||
"""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, QueryDict
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.base import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, Provider, User
|
||||
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.flows.views.executor import SESSION_KEY_APPLICATION_PRE, 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):
|
||||
@ -139,65 +125,3 @@ 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 BufferedPolicyAccessView, RequestValidationError
|
||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||
from authentik.providers.oauth2.constants import (
|
||||
PKCE_METHOD_PLAIN,
|
||||
PKCE_METHOD_S256,
|
||||
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
|
||||
return code
|
||||
|
||||
|
||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
||||
class AuthorizationFlowInitView(PolicyAccessView):
|
||||
"""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 BufferedPolicyAccessView
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||
|
||||
|
||||
class RACStartView(BufferedPolicyAccessView):
|
||||
class RACStartView(PolicyAccessView):
|
||||
"""Start a RAC connection by checking access and creating a connection token"""
|
||||
|
||||
endpoint: Endpoint
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-31 13:50
|
||||
|
||||
import authentik.lib.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="acs_url",
|
||||
field=models.TextField(
|
||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name="ACS URL",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.api.object_types import CreatableType
|
||||
from authentik.core.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,
|
||||
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
|
||||
class SAMLProvider(Provider):
|
||||
"""SAML 2.0 Endpoint for applications which support SAML."""
|
||||
|
||||
acs_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
|
||||
)
|
||||
acs_url = models.URLField(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 BufferedPolicyAccessView
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
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(BufferedPolicyAccessView):
|
||||
class SAMLSSOView(PolicyAccessView):
|
||||
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
||||
Calls get/post handler."""
|
||||
|
||||
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
"""GET and POST use the same handler, but we can't
|
||||
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
|
||||
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||
return self.get(request, application_slug)
|
||||
|
||||
|
||||
|
||||
@ -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,11 +1,13 @@
|
||||
"""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,
|
||||
@ -13,20 +15,33 @@ from authentik.sources.kerberos.models import (
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = UserKerberosSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
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, ModelViewSet):
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupKerberosSourceConnection.objects.all()
|
||||
serializer_class = GroupKerberosSourceConnectionSerializer
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
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,6 +372,8 @@ 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,7 +99,6 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||
"sync_groups",
|
||||
"sync_parent_group",
|
||||
"connectivity",
|
||||
"lookup_groups_from_user",
|
||||
]
|
||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||
|
||||
@ -135,7 +134,6 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"sync_parent_group",
|
||||
"user_property_mappings",
|
||||
"group_property_mappings",
|
||||
"lookup_groups_from_user",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
# 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,14 +123,6 @@ 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,17 +28,15 @@ 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=attributes,
|
||||
attributes=[
|
||||
self._source.group_membership_field,
|
||||
self._source.object_uniqueness_field,
|
||||
LDAP_DISTINGUISHED_NAME,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@ -49,24 +47,9 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
return -1
|
||||
membership_count = 0
|
||||
for group in page_data:
|
||||
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, [])
|
||||
|
||||
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
|
||||
@ -85,7 +68,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"ak_groups__in": [ak_group],
|
||||
}
|
||||
)
|
||||
).distinct()
|
||||
)
|
||||
membership_count += 1
|
||||
membership_count += users.count()
|
||||
ak_group.users.set(users)
|
||||
|
||||
@ -96,26 +96,6 @@ 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,43 +162,6 @@ 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,3 +1,5 @@
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -10,9 +12,11 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
|
||||
|
||||
|
||||
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserOAuthSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier", "access_token"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"access_token": {"write_only": True},
|
||||
@ -20,15 +24,21 @@ 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
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
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,6 +286,7 @@ 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,3 +1,5 @@
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -10,9 +12,14 @@ 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 + ["plex_token"]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + [
|
||||
"identifier",
|
||||
"plex_token",
|
||||
]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"plex_token": {"write_only": True},
|
||||
@ -20,15 +27,21 @@ 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
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
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,6 +141,7 @@ class UserPlexSourceConnection(UserSourceConnection):
|
||||
"""Connect user and plex source"""
|
||||
|
||||
plex_token = models.TextField()
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -10,20 +12,29 @@ 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, ModelViewSet):
|
||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupSAMLSourceConnection.objects.all()
|
||||
serializer_class = GroupSAMLSourceConnectionSerializer
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-31 13:53
|
||||
|
||||
import authentik.lib.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_saml", "0017_fix_x509subjectname"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="slo_url",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Optional URL if your IDP supports Single-Logout.",
|
||||
null=True,
|
||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name="SLO URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="sso_url",
|
||||
field=models.TextField(
|
||||
help_text="URL that the initial Login request is sent to.",
|
||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name="SSO URL",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,26 +0,0 @@
|
||||
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,7 +20,6 @@ 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,
|
||||
@ -92,13 +91,11 @@ class SAMLSource(Source):
|
||||
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
||||
)
|
||||
|
||||
sso_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
sso_url = models.URLField(
|
||||
verbose_name=_("SSO URL"),
|
||||
help_text=_("URL that the initial Login request is sent to."),
|
||||
)
|
||||
slo_url = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
slo_url = models.URLField(
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -318,6 +315,8 @@ 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
|
||||
|
||||
@ -323,12 +323,7 @@
|
||||
"multiValued": false,
|
||||
"description": "Indicates whether or not an attribute is modifiable.",
|
||||
"required": false,
|
||||
"canonicalValues": [
|
||||
"readOnly",
|
||||
"readWrite",
|
||||
"immutable",
|
||||
"writeOnly"
|
||||
],
|
||||
"canonicalValues": ["readOnly", "readWrite", "immutable", "writeOnly"],
|
||||
"caseExact": false,
|
||||
"mutability": "readOnly",
|
||||
"returned": "default",
|
||||
@ -340,12 +335,7 @@
|
||||
"multiValued": false,
|
||||
"description": "Indicates when an attribute is returned in a response (e.g., to a query).",
|
||||
"required": false,
|
||||
"canonicalValues": [
|
||||
"always",
|
||||
"never",
|
||||
"default",
|
||||
"request"
|
||||
],
|
||||
"canonicalValues": ["always", "never", "default", "request"],
|
||||
"caseExact": false,
|
||||
"mutability": "readOnly",
|
||||
"returned": "default",
|
||||
@ -369,12 +359,7 @@
|
||||
"multiValued": true,
|
||||
"description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.",
|
||||
"required": false,
|
||||
"canonicalValues": [
|
||||
"resource",
|
||||
"external",
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"canonicalValues": ["resource", "external", "uri", "url"],
|
||||
"caseExact": false,
|
||||
"mutability": "readOnly",
|
||||
"returned": "default",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -104,13 +104,6 @@ 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, create_test_user
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
@ -67,67 +67,6 @@ 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,14 +32,7 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
sanitized_to = []
|
||||
# Ensure that all recipients are valid
|
||||
for recipient_name, recipient_email in to:
|
||||
# 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"))
|
||||
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
||||
super().__init__(to=sanitized_to, **kwargs)
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
@ -142,38 +142,35 @@ 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
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||
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.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
||||
10
eslint.config.mjs
Normal file
10
eslint.config.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* ESLint configuration for authentik's monorepo.
|
||||
*/
|
||||
const ESLintConfig = createESLintPackageConfig();
|
||||
|
||||
export default ESLintConfig;
|
||||
17
go.mod
17
go.mod
@ -1,10 +1,12 @@
|
||||
module goauthentik.io
|
||||
|
||||
go 1.24.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/coreos/go-oidc/v3 v3.13.0
|
||||
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
|
||||
@ -20,17 +22,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.22.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
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.2025024.1
|
||||
goauthentik.io/api/v3 v3.2025022.6
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
@ -60,6 +62,7 @@ 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
|
||||
@ -76,6 +79,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.5 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // 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.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
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/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,9 +148,8 @@ 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=
|
||||
@ -208,8 +207,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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
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=
|
||||
@ -240,8 +239,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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
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_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=
|
||||
@ -300,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.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.2025024.1 h1:wYmpbNW1XptrjS5dlnZj8CrCs+JUGEVJYStrFdWL9aA=
|
||||
goauthentik.io/api/v3 v3.2025024.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
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=
|
||||
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=
|
||||
@ -396,8 +395,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.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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/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=
|
||||
@ -412,8 +411,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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.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=
|
||||
@ -600,8 +599,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.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
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,14 +162,13 @@ func (c *Config) parseScheme(rawVal string) string {
|
||||
if err != nil {
|
||||
return rawVal
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "env":
|
||||
if u.Scheme == "env" {
|
||||
e, ok := os.LookupEnv(u.Host)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return u.RawQuery
|
||||
case "file":
|
||||
} else if u.Scheme == "file" {
|
||||
d, err := os.ReadFile(u.Path)
|
||||
if err != nil {
|
||||
return u.RawQuery
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigEnv(t *testing.T) {
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar"))
|
||||
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) {
|
||||
assert.NoError(t, os.Setenv("foo", "bar"))
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo"))
|
||||
os.Setenv("foo", "bar")
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
@ -33,15 +33,13 @@ func TestConfigEnv_File(t *testing.T) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, os.Remove(file.Name()))
|
||||
}()
|
||||
defer os.Remove(file.Name())
|
||||
_, err = file.Write([]byte("bar"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())))
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
|
||||
cfg = nil
|
||||
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.4"
|
||||
const VERSION = "2025.2.2"
|
||||
|
||||
5
internal/crypto/backend/fips_disabled.go
Normal file
5
internal/crypto/backend/fips_disabled.go
Normal file
@ -0,0 +1,5 @@
|
||||
//go:build requirefips
|
||||
|
||||
package backend
|
||||
|
||||
var FipsEnabled = true
|
||||
5
internal/crypto/backend/fips_enabled.go
Normal file
5
internal/crypto/backend/fips_enabled.go
Normal file
@ -0,0 +1,5 @@
|
||||
//go:build !requirefips
|
||||
|
||||
package backend
|
||||
|
||||
var FipsEnabled = false
|
||||
@ -35,7 +35,7 @@ func EnableDebugServer() {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl)
|
||||
_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("failed to write index")
|
||||
return nil
|
||||
|
||||
@ -44,11 +44,10 @@ func New(healthcheck func() bool) *GoUnicorn {
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
if sig == syscall.SIGHUP {
|
||||
g.log.Info("SIGHUP received, forwarding to gunicorn")
|
||||
g.Reload()
|
||||
case syscall.SIGUSR2:
|
||||
} else if sig == syscall.SIGUSR2 {
|
||||
g.log.Info("SIGUSR2 received, restarting gunicorn")
|
||||
g.Restart()
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package ak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
|
||||
"golangVersion": runtime.Version(),
|
||||
"opensslEnabled": cryptobackend.OpensslEnabled,
|
||||
"opensslVersion": cryptobackend.OpensslVersion(),
|
||||
"fipsEnabled": fips140.Enabled(),
|
||||
"fipsEnabled": cryptobackend.FipsEnabled,
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err == nil {
|
||||
|
||||
@ -35,19 +35,13 @@ 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, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
res, _, 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
|
||||
}
|
||||
@ -57,9 +51,6 @@ 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,64 +1,5 @@
|
||||
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,8 +148,7 @@ func (ac *APIController) startWSHandler() {
|
||||
"outpost_type": ac.Server.Type(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
}).Set(1)
|
||||
switch wsMsg.Instruction {
|
||||
case WebsocketInstructionTriggerUpdate:
|
||||
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
|
||||
time.Sleep(ac.reloadOffset)
|
||||
logger.Debug("Got update trigger...")
|
||||
err := ac.OnRefresh()
|
||||
@ -164,7 +163,7 @@ func (ac *APIController) startWSHandler() {
|
||||
"build": constants.BUILD(""),
|
||||
}).SetToCurrentTime()
|
||||
}
|
||||
case WebsocketInstructionProviderSpecific:
|
||||
} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
|
||||
for _, h := range ac.wsHandlers {
|
||||
h(context.Background(), wsMsg.Args)
|
||||
}
|
||||
|
||||
@ -66,12 +66,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
||||
return err
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
|
||||
@ -49,12 +49,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
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.Scope
|
||||
scope := req.SearchRequest.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 = aku
|
||||
var newHost *url.URL = aku
|
||||
var newBrowserHost *url.URL
|
||||
if embedded {
|
||||
if authentikHost == "" {
|
||||
|
||||
@ -130,12 +130,7 @@ func (ps *ProxyServer) ServeHTTP() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
||||
ps.serve(proxyListener)
|
||||
@ -154,12 +149,7 @@ func (ps *ProxyServer) ServeHTTPS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")
|
||||
|
||||
@ -72,13 +72,11 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
|
||||
session.ID = c.Value
|
||||
|
||||
err = s.load(r.Context(), session)
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return session, nil
|
||||
}
|
||||
return session, err
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else if err == redis.Nil {
|
||||
err = nil // no data stored
|
||||
}
|
||||
session.IsNew = false
|
||||
return session, err
|
||||
}
|
||||
|
||||
|
||||
@ -156,12 +156,7 @@ func (ws *WebServer) listenPlain() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
||||
ws.serve(proxyListener)
|
||||
|
||||
@ -46,12 +46,7 @@ func (ws *WebServer) listenTLS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
defer proxyListener.Close()
|
||||
|
||||
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} docker.io/library/golang:1.24-bookworm AS builder
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG 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 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" 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.1007.0",
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"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==",
|
||||
"version": "2.1005.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
|
||||
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@goauthentik/lifecycle-aws",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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.4
|
||||
Default: 2025.2.2
|
||||
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-31 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 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,20 +1220,6 @@ 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-31 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 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,22 +1347,6 @@ 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-03-31 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-02-14 14:49+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2024\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,10 +130,6 @@ 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 "
|
||||
@ -181,14 +177,6 @@ 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."
|
||||
@ -272,11 +260,11 @@ msgstr "Applicazioni"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlement"
|
||||
msgstr "Entitlement Applicazione"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlements"
|
||||
msgstr "Entitlements Applicazione"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the source-specific identifier"
|
||||
@ -563,6 +551,62 @@ 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"
|
||||
@ -570,39 +614,39 @@ msgstr "Chiave di firma"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Key used to sign the SSF Events."
|
||||
msgstr "Chiave utilizzata per firmare gli eventi SSF."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Provider"
|
||||
msgstr "Fornitore Shared Signals Framework"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Providers"
|
||||
msgstr "Fornitori Shared Signals Framework"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Add stream to SSF provider"
|
||||
msgstr "Aggiungi Stream al provider SSF"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream"
|
||||
msgstr "SSF Stream"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Streams"
|
||||
msgstr "SSF Streams"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Event"
|
||||
msgstr "Evento di Stream SSF"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Events"
|
||||
msgstr "Eventi di Stream SSF"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr "Impossibile inviare la richiesta"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@ -668,26 +712,9 @@ 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 "
|
||||
@ -917,7 +944,7 @@ msgstr ""
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid "Evaluate policies when the Stage is presented to the user."
|
||||
msgstr "Valutare i criteri quando la fase viene presentata all'utente."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid ""
|
||||
@ -959,14 +986,6 @@ 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"
|
||||
@ -981,10 +1000,6 @@ 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}"
|
||||
@ -1197,14 +1212,6 @@ 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"
|
||||
@ -1337,22 +1344,6 @@ 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"
|
||||
@ -1540,14 +1531,6 @@ 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"
|
||||
@ -1831,61 +1814,6 @@ 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."
|
||||
@ -1973,20 +1901,6 @@ 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: "
|
||||
@ -2128,18 +2042,6 @@ 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"
|
||||
@ -2148,16 +2050,6 @@ 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"
|
||||
@ -2233,7 +2125,7 @@ msgstr ""
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "Tipo server KAdmin"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
@ -2838,117 +2730,6 @@ 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 "
|
||||
@ -2987,6 +2768,11 @@ 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"
|
||||
@ -3227,10 +3013,23 @@ 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"
|
||||
@ -3239,6 +3038,12 @@ 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."
|
||||
@ -3322,6 +3127,17 @@ 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"
|
||||
@ -3342,6 +3158,11 @@ 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"
|
||||
@ -3671,7 +3492,6 @@ 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-31 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 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,20 +1234,6 @@ 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 "权限被拒绝"
|
||||
|
||||
43195
package-lock.json
generated
43195
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user