mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
66 Commits
devcontain
...
flows/corr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1db6104bef | ||
|
|
62dc04a684 | ||
|
|
68f3bf6ec1 | ||
|
|
8234613b76 | ||
|
|
eec998cc8d | ||
|
|
d01aa6bebf | ||
|
|
cbbf315662 | ||
|
|
45ca767fd8 | ||
|
|
5d3e2e89e0 | ||
|
|
5e2f261a0c | ||
|
|
10a421e678 | ||
|
|
668ad3dadf | ||
|
|
e7903d5391 | ||
|
|
e38fffc44c | ||
|
|
4bc2bca448 | ||
|
|
48916303d8 | ||
|
|
d28109da6a | ||
|
|
3bd299d52a | ||
|
|
57418582c5 | ||
|
|
f37958bcd0 | ||
|
|
8931b621b4 | ||
|
|
9d3d96bab1 | ||
|
|
712f0ed95e | ||
|
|
1cd9c7bf9d | ||
|
|
fb23751079 | ||
|
|
e49aace000 | ||
|
|
876b299f30 | ||
|
|
458439c396 | ||
|
|
d3d0effe9d | ||
|
|
413b073191 | ||
|
|
46747ae3f2 | ||
|
|
d64a3aab39 | ||
|
|
970cddae47 | ||
|
|
24c4495ac2 | ||
|
|
ff38607fa3 | ||
|
|
eef8e57f6c | ||
|
|
603820854b | ||
|
|
4ad7f8be2a | ||
|
|
a605cd1e87 | ||
|
|
936789f534 | ||
|
|
2f52d832ab | ||
|
|
036514730e | ||
|
|
d48129ba7b | ||
|
|
d219f72ed6 | ||
|
|
7b19045431 | ||
|
|
0027813e4b | ||
|
|
a6ebf1074f | ||
|
|
ea9689c493 | ||
|
|
06e7335618 | ||
|
|
42c4fee053 | ||
|
|
26cfbe67f3 | ||
|
|
2a17024afc | ||
|
|
c557b55e0e | ||
|
|
f56e354e38 | ||
|
|
c50c2b0e0c | ||
|
|
662124cac9 | ||
|
|
3d671a901b | ||
|
|
a7fb031b64 | ||
|
|
2818b0bbdf | ||
|
|
60075e39fb | ||
|
|
c112f702b3 | ||
|
|
42b3323b3d | ||
|
|
78380831de | ||
|
|
8b5195aeff | ||
|
|
d762e38027 | ||
|
|
e427cb611e |
@@ -1,63 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Start from the same FIPS Python base as production (python-base stage)
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff
|
||||
|
||||
USER root
|
||||
|
||||
# Setup environment matching production python-base stage
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
# Copy uv package manager
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.7@sha256:ba4857bf2a068e9bc0e64eed8563b065908a4cd6bfb66b531a9c424c8e25e142 /uv /uvx /bin/
|
||||
|
||||
# Install build dependencies
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential pkg-config libffi-dev git binutils \
|
||||
# cryptography
|
||||
curl \
|
||||
# libxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# postgresql
|
||||
libpq-dev \
|
||||
# python-kadmin-rs and kerberos testing
|
||||
clang libkrb5-dev sccache krb5-kdc krb5-admin-server \
|
||||
# xmlsec
|
||||
libltdl-dev \
|
||||
# runit (for chpst command used by lifecycle/ak)
|
||||
runit \
|
||||
# sudo (required by devcontainer features)
|
||||
sudo && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Environment for building native Python packages
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
# Create authentik user with proper home directory (required for devcontainer features)
|
||||
RUN adduser --disabled-password --gecos "" --uid 1000 --home /home/authentik authentik && \
|
||||
mkdir -p /certs /media /ak-root && \
|
||||
chown -R authentik:authentik /certs /media /ak-root /home/authentik && \
|
||||
echo "authentik ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/authentik
|
||||
|
||||
# FIPS configuration for Go development
|
||||
# Don't set GOFIPS/GOFIPS140 globally to avoid breaking Go tools like docker-compose
|
||||
# These will be set when building/running authentik Go code (see lifecycle/ak and Makefile)
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
# Set TMPDIR for PID files and temp data
|
||||
# Use /tmp instead of /dev/shm for development because go run needs to execute binaries
|
||||
ENV TMPDIR=/tmp
|
||||
|
||||
USER authentik
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "authentik",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/ak-root",
|
||||
"containerUser": "authentik",
|
||||
"remoteUser": "authentik",
|
||||
"shutdownAction": "stopCompose",
|
||||
"containerEnv": {
|
||||
"LOCAL_PROJECT_DIR": "/ak-root"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "1.24"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "24"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "latest",
|
||||
"moby": false
|
||||
}
|
||||
},
|
||||
"mounts": [],
|
||||
"forwardPorts": [9000, 9443],
|
||||
"portsAttributes": {
|
||||
"8000": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"3963": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"35151": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"9901": {
|
||||
"onAutoForward": "ignore"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"bashmish.es6-string-css",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"golang.go",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.pylint",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"charliermarsh.ruff"
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/ak-root/.venv/bin/python",
|
||||
"python.terminal.activateEnvironment": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
user: authentik
|
||||
privileged: true
|
||||
volumes:
|
||||
- ../:/ak-root
|
||||
entrypoint: []
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
PATH: "/ak-root/.venv/bin:${PATH}"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9443:9443"
|
||||
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d authentik -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
env_file: .env
|
||||
command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
|
||||
|
||||
s3:
|
||||
image: docker.io/zenko/cloudserver
|
||||
env_file: .env
|
||||
environment:
|
||||
REMOTE_MANAGEMENT_DISABLE: "1"
|
||||
ports:
|
||||
- "8020:8000"
|
||||
volumes:
|
||||
- s3-data:/usr/src/app/localData
|
||||
- s3-metadata:/usr/src/app/localMetadata
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
s3-data:
|
||||
s3-metadata:
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "Running authentik devcontainer setup"
|
||||
echo "======================================"
|
||||
|
||||
echo ""
|
||||
echo "Step 1/5: Installing dependencies"
|
||||
make install
|
||||
|
||||
echo ""
|
||||
echo "Step 2/5: Generating development config"
|
||||
make gen-dev-config
|
||||
|
||||
echo ""
|
||||
echo "Step 3/5: Running database migrations"
|
||||
make migrate
|
||||
|
||||
echo ""
|
||||
echo "Step 4/5: Generating API clients"
|
||||
make gen
|
||||
|
||||
echo ""
|
||||
echo "Step 5/5: Building web assets"
|
||||
make web
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setup complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "You can now run:"
|
||||
echo " - 'make run-server' to start the backend server"
|
||||
echo " - 'make run-worker' to start the worker (must be ran once after initial setup)"
|
||||
echo " - 'make web-watch' for live web development"
|
||||
echo ""
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -21,7 +21,7 @@ runs:
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v5
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
|
||||
4
.github/actions/test-results/action.yml
vendored
4
.github/actions/test-results/action.yml
vendored
@@ -12,11 +12,11 @@ runs:
|
||||
with:
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
flags: ${{ inputs.flags }}
|
||||
file: unittest.xml
|
||||
use_oidc: true
|
||||
report_type: test_results
|
||||
- name: PostgreSQL Logs
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
6
.github/workflows/ci-api-docs.yml
vendored
6
.github/workflows/ci-api-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
|
||||
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/website/api/.docusaurus
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build -w api
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v5
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
|
||||
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@@ -201,7 +201,7 @@ jobs:
|
||||
run: |
|
||||
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
|
||||
4
.github/workflows/release-tag.yml
vendored
4
.github/workflows/release-tag.yml
vendored
@@ -49,8 +49,12 @@ jobs:
|
||||
test:
|
||||
name: Pre-release test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-inputs
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
|
||||
- run: make test-docker
|
||||
bump-authentik:
|
||||
name: Bump authentik version
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN npm run build && \
|
||||
npm run build:sfe
|
||||
|
||||
# Stage 2: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.17@sha256:5cb6b54d2bc3fe2eb9a8483db958a0b9eebf9edff68adedb369df8e7b98711a2 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.18@sha256:5713fa8217f92b80223bc83aac7db36ec80a84437dbc0d04bbc659cae030d8c9 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base
|
||||
|
||||
|
||||
17
Makefile
17
Makefile
@@ -9,6 +9,13 @@ NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
PY_SOURCES = authentik packages tests scripts lifecycle .github
|
||||
DOCKER_IMAGE ?= "authentik:test"
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE = sed -i ''
|
||||
else
|
||||
SED_INPLACE = sed -i
|
||||
endif
|
||||
|
||||
GEN_API_TS = gen-ts-api
|
||||
GEN_API_PY = gen-py-api
|
||||
GEN_API_GO = gen-go-api
|
||||
@@ -46,7 +53,7 @@ help: ## Show this help
|
||||
@echo ""
|
||||
|
||||
go-test:
|
||||
GOFIPS140=latest CGO_ENABLED=1 go test -timeout 0 -v -race -cover ./...
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
$(KRB_PATH) uv run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
|
||||
@@ -119,8 +126,8 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
$(error Usage: make bump version=20xx.xx.xx )
|
||||
endif
|
||||
sed -i 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
sed -i 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
npm version --no-git-tag-version --allow-same-version $(version)
|
||||
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
|
||||
@@ -155,8 +162,8 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a
|
||||
/local/schema-old.yml \
|
||||
/local/schema.yml
|
||||
rm schema-old.yml
|
||||
sed -i 's/{/{/g' diff.md
|
||||
sed -i 's/}/}/g' diff.md
|
||||
$(SED_INPLACE) 's/{/{/g' diff.md
|
||||
$(SED_INPLACE) 's/}/}/g' diff.md
|
||||
npx prettier --write diff.md
|
||||
|
||||
gen-clean-ts: ## Remove generated API client for TypeScript
|
||||
|
||||
@@ -37,7 +37,7 @@ class VersionSerializer(PassiveSerializer):
|
||||
|
||||
def get_version_latest(self, _) -> str:
|
||||
"""Get latest version from cache"""
|
||||
if get_current_tenant().schema_name == get_public_schema_name():
|
||||
if get_current_tenant().schema_name != get_public_schema_name():
|
||||
return authentik_version()
|
||||
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||
if not version_in_cache: # pragma: no cover
|
||||
|
||||
@@ -240,7 +240,9 @@ class FileUsedByView(APIView):
|
||||
for field in fields:
|
||||
q |= Q(**{field: params.get("name")})
|
||||
|
||||
objs = get_objects_for_user(request.user, f"{app}.view_{model_name}", model)
|
||||
objs = get_objects_for_user(
|
||||
request.user, f"{app}.view_{model_name}", model.objects.all()
|
||||
)
|
||||
objs = objs.filter(q)
|
||||
for obj in objs:
|
||||
serializer = UsedBySerializer(
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class AuthentikFilesConfig(ManagedAppConfig):
|
||||
@@ -11,20 +6,3 @@ class AuthentikFilesConfig(ManagedAppConfig):
|
||||
label = "authentik_admin_files"
|
||||
verbose_name = "authentik Files"
|
||||
default = True
|
||||
|
||||
@ManagedAppConfig.reconcile_global
|
||||
def check_for_media_mount(self):
|
||||
if settings.TEST:
|
||||
return
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
if (
|
||||
CONFIG.get("storage.media.backend", CONFIG.get("storage.backend", "file")) == "file"
|
||||
and Path("/media").exists()
|
||||
):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="/media has been moved to /data/media. "
|
||||
"Check the release notes for migration steps.",
|
||||
).save()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from unittest import skipUnless
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
@skipUnless(s3_test_server_available(), "S3 test server not available")
|
||||
class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
"""Test S3 backend functionality"""
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Test file service layer"""
|
||||
|
||||
from unittest import skipUnless
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.manager import FileManager
|
||||
from authentik.admin.files.tests.utils import FileTestFileBackendMixin, FileTestS3BackendMixin
|
||||
from authentik.admin.files.tests.utils import (
|
||||
FileTestFileBackendMixin,
|
||||
FileTestS3BackendMixin,
|
||||
s3_test_server_available,
|
||||
)
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
@@ -81,6 +87,7 @@ class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
|
||||
self.assertEqual(result, "http://example.com/files/media/public/test.png")
|
||||
|
||||
|
||||
@skipUnless(s3_test_server_available(), "S3 test server not available")
|
||||
class TestResolveFileUrlS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
@CONFIG.patch("storage.media.s3.custom_domain", "s3.test:8080/test")
|
||||
@CONFIG.patch("storage.media.s3.secure_urls", False)
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import shutil
|
||||
import socket
|
||||
from tempfile import mkdtemp
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from authentik.admin.files.backends.s3 import S3Backend
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG, UNSET
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
S3_TEST_ENDPOINT = "http://localhost:8020"
|
||||
|
||||
|
||||
def s3_test_server_available() -> bool:
|
||||
"""Check if the S3 test server is reachable."""
|
||||
|
||||
parsed = urlparse(S3_TEST_ENDPOINT)
|
||||
try:
|
||||
with socket.create_connection((parsed.hostname, parsed.port), timeout=2):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
class FileTestFileBackendMixin:
|
||||
def setUp(self):
|
||||
@@ -57,7 +72,7 @@ class FileTestS3BackendMixin:
|
||||
for key in s3_config_keys:
|
||||
self.original_media_s3_settings[key] = CONFIG.get(f"storage.media.s3.{key}", UNSET)
|
||||
self.media_s3_bucket_name = f"authentik-test-{generate_id(10)}".lower()
|
||||
CONFIG.set("storage.media.s3.endpoint", "http://localhost:8020")
|
||||
CONFIG.set("storage.media.s3.endpoint", S3_TEST_ENDPOINT)
|
||||
CONFIG.set("storage.media.s3.access_key", "accessKey1")
|
||||
CONFIG.set("storage.media.s3.secret_key", "secretKey1")
|
||||
CONFIG.set("storage.media.s3.bucket_name", self.media_s3_bucket_name)
|
||||
@@ -70,7 +85,7 @@ class FileTestS3BackendMixin:
|
||||
for key in s3_config_keys:
|
||||
self.original_reports_s3_settings[key] = CONFIG.get(f"storage.reports.s3.{key}", UNSET)
|
||||
self.reports_s3_bucket_name = f"authentik-test-{generate_id(10)}".lower()
|
||||
CONFIG.set("storage.reports.s3.endpoint", "http://localhost:8020")
|
||||
CONFIG.set("storage.reports.s3.endpoint", S3_TEST_ENDPOINT)
|
||||
CONFIG.set("storage.reports.s3.access_key", "accessKey1")
|
||||
CONFIG.set("storage.reports.s3.secret_key", "secretKey1")
|
||||
CONFIG.set("storage.reports.s3.bucket_name", self.reports_s3_bucket_name)
|
||||
|
||||
@@ -15,7 +15,9 @@ class Pagination(pagination.PageNumberPagination):
|
||||
|
||||
def get_page_size(self, request):
|
||||
if self.page_size_query_param in request.query_params:
|
||||
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
|
||||
page_size = super().get_page_size(request)
|
||||
if page_size is not None:
|
||||
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
|
||||
return request.tenant.pagination_default_page_size
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
|
||||
@@ -180,10 +180,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
|
||||
def _filter_applications_with_launch_url(
|
||||
self, applications: QuerySet[Application]
|
||||
self, paginated_apps: QuerySet[Application]
|
||||
) -> list[Application]:
|
||||
applications = []
|
||||
for app in applications:
|
||||
for app in paginated_apps:
|
||||
if app.get_launch_url():
|
||||
applications.append(app)
|
||||
return applications
|
||||
|
||||
@@ -33,6 +33,16 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"email",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class PartialUserSerializer(ModelSerializer):
|
||||
"""Partial User Serializer, does not include child relations."""
|
||||
@@ -42,16 +52,7 @@ class PartialUserSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"email",
|
||||
"attributes",
|
||||
"uid",
|
||||
]
|
||||
fields = PARTIAL_USER_SERIALIZER_MODEL_FIELDS + ["uid"]
|
||||
|
||||
|
||||
class RelatedGroupSerializer(ModelSerializer):
|
||||
@@ -262,7 +263,14 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
base_qs = Group.objects.all().prefetch_related("roles")
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_users:
|
||||
base_qs = base_qs.prefetch_related("users")
|
||||
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
|
||||
# time
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch(
|
||||
"users",
|
||||
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
|
||||
)
|
||||
)
|
||||
else:
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch("users", queryset=User.objects.all().only("id"))
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
@@ -145,12 +144,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
owner_field = "user"
|
||||
rbac_allow_create_without_perm = True
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user if self.request else get_anonymous_user()
|
||||
if user.is_superuser:
|
||||
return super().get_queryset()
|
||||
return super().get_queryset().filter(user=user.pk)
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
if not self.request.user.is_superuser:
|
||||
instance = serializer.save(
|
||||
|
||||
@@ -18,10 +18,9 @@ def migrate_object_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
|
||||
RoleModelPermission = apps.get_model("guardian", "RoleModelPermission")
|
||||
|
||||
def get_role_for_user_id(user_id: int) -> Role:
|
||||
name = f"ak-managed-role--user-{user_id}"
|
||||
name = f"ak-migrated-role--user-{user_id}"
|
||||
role, created = Role.objects.using(db_alias).get_or_create(
|
||||
name=name,
|
||||
managed=name,
|
||||
)
|
||||
if created:
|
||||
role.users.add(user_id)
|
||||
@@ -32,11 +31,10 @@ def migrate_object_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
|
||||
if not role:
|
||||
# Every django group should already have a role, so this should never happen.
|
||||
# But let's be nice.
|
||||
name = f"ak-managed-role--group-{group_id}"
|
||||
name = f"ak-migrated-role--group-{group_id}"
|
||||
role, created = Role.objects.using(db_alias).get_or_create(
|
||||
group_id=group_id,
|
||||
name=name,
|
||||
managed=name,
|
||||
)
|
||||
if created:
|
||||
role.group_id = group_id
|
||||
|
||||
@@ -183,16 +183,16 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(len(body["results"]), 1)
|
||||
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||
|
||||
def test_list_admin(self):
|
||||
"""Test Token List (Test with admin auth)"""
|
||||
def test_list_with_permission(self):
|
||||
"""Test Token List (Test with `view_token` permission)"""
|
||||
Token.objects.all().delete()
|
||||
self.client.force_login(self.admin)
|
||||
token_should: Token = Token.objects.create(
|
||||
identifier="test", expiring=False, user=self.user
|
||||
)
|
||||
token_should_not: Token = Token.objects.create(
|
||||
identifier="test-2", expiring=False, user=get_anonymous_user()
|
||||
)
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_token")
|
||||
response = self.client.get(reverse("authentik_api:token-list"))
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Crypto API Views"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
@@ -15,14 +13,12 @@ from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
)
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
ChoiceField,
|
||||
DateTimeField,
|
||||
IntegerField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
@@ -51,59 +47,15 @@ LOGGER = get_logger()
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
|
||||
fingerprint_sha256 = SerializerMethodField()
|
||||
fingerprint_sha1 = SerializerMethodField()
|
||||
|
||||
cert_expiry = SerializerMethodField()
|
||||
cert_subject = SerializerMethodField()
|
||||
private_key_available = SerializerMethodField()
|
||||
key_type = SerializerMethodField()
|
||||
|
||||
certificate_download_url = SerializerMethodField()
|
||||
private_key_download_url = SerializerMethodField()
|
||||
|
||||
@property
|
||||
def _should_include_details(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_details", "true")).lower() == "true"
|
||||
|
||||
def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> str | None:
|
||||
"Get certificate Hash (SHA256)"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.fingerprint_sha256
|
||||
|
||||
def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> str | None:
|
||||
"Get certificate Hash (SHA1)"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.fingerprint_sha1
|
||||
|
||||
def get_cert_expiry(self, instance: CertificateKeyPair) -> datetime | None:
|
||||
"Get certificate expiry"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return DateTimeField().to_representation(instance.certificate.not_valid_after_utc)
|
||||
|
||||
def get_cert_subject(self, instance: CertificateKeyPair) -> str | None:
|
||||
"""Get certificate subject as full rfc4514"""
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.certificate.subject.rfc4514_string()
|
||||
|
||||
def get_private_key_available(self, instance: CertificateKeyPair) -> bool:
|
||||
"""Show if this keypair has a private key configured or not"""
|
||||
return instance.key_data != "" and instance.key_data is not None
|
||||
|
||||
@extend_schema_field(ChoiceField(choices=KeyType.choices, allow_null=True))
|
||||
def get_key_type(self, instance: CertificateKeyPair) -> str | None:
|
||||
"""Get the key algorithm type from the certificate's public key"""
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.key_type
|
||||
|
||||
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get URL to download certificate"""
|
||||
return (
|
||||
@@ -175,6 +127,11 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"managed": {"read_only": True},
|
||||
"key_data": {"write_only": True},
|
||||
"certificate_data": {"write_only": True},
|
||||
"fingerprint_sha256": {"read_only": True},
|
||||
"fingerprint_sha1": {"read_only": True},
|
||||
"cert_expiry": {"read_only": True},
|
||||
"cert_subject": {"read_only": True},
|
||||
"key_type": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@@ -216,17 +173,12 @@ class CertificateKeyPairFilter(FilterSet):
|
||||
return queryset.exclude(key_data__exact="")
|
||||
|
||||
def filter_key_type(self, queryset, name, value): # pragma: no cover
|
||||
"""Filter certificates by key type using the public key from the certificate"""
|
||||
"""Filter certificates by key type using the stored database field"""
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
# value is a list of KeyType enum values from MultipleChoiceFilter
|
||||
filtered_pks = []
|
||||
for cert in queryset:
|
||||
if cert.key_type in value:
|
||||
filtered_pks.append(cert.pk)
|
||||
|
||||
return queryset.filter(pk__in=filtered_pks)
|
||||
return queryset.filter(key_type__in=value)
|
||||
|
||||
class Meta:
|
||||
model = CertificateKeyPair
|
||||
@@ -263,7 +215,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
"Can be specified multiple times (e.g. '?key_type=rsa&key_type=ec')"
|
||||
),
|
||||
),
|
||||
OpenApiParameter("include_details", bool, default=True),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-09 06:22
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from django.db import migrations, models
|
||||
|
||||
from authentik.crypto.signals import extract_certificate_metadata
|
||||
|
||||
|
||||
def backfill_certificate_metadata(apps, schema_editor): # noqa: ARG001
|
||||
"""Backfill certificate metadata and kid for existing records."""
|
||||
|
||||
CertificateKeyPair = apps.get_model("authentik_crypto", "CertificateKeyPair")
|
||||
|
||||
for cert in CertificateKeyPair.objects.all():
|
||||
updated_fields = []
|
||||
|
||||
if cert.certificate_data:
|
||||
try:
|
||||
certificate = load_pem_x509_certificate(
|
||||
cert.certificate_data.encode("utf-8"), default_backend()
|
||||
)
|
||||
metadata = extract_certificate_metadata(certificate)
|
||||
|
||||
cert.key_type = metadata["key_type"]
|
||||
cert.cert_expiry = metadata["cert_expiry"]
|
||||
cert.cert_subject = metadata["cert_subject"]
|
||||
cert.fingerprint_sha256 = metadata["fingerprint_sha256"]
|
||||
cert.fingerprint_sha1 = metadata["fingerprint_sha1"]
|
||||
updated_fields.extend(
|
||||
[
|
||||
"key_type",
|
||||
"cert_expiry",
|
||||
"cert_subject",
|
||||
"fingerprint_sha256",
|
||||
"fingerprint_sha1",
|
||||
]
|
||||
)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
# Backfill kid with MD5 for backwards compatibility
|
||||
if cert.key_data:
|
||||
cert.kid = md5(cert.key_data.encode("utf-8"), usedforsecurity=False).hexdigest()
|
||||
updated_fields.append("kid")
|
||||
|
||||
if updated_fields:
|
||||
cert.save(update_fields=updated_fields)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0005_alter_certificatekeypair_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="cert_expiry",
|
||||
field=models.DateTimeField(blank=True, help_text="Certificate expiry date", null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="cert_subject",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Certificate subject as RFC4514 string", null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="fingerprint_sha1",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="SHA1 fingerprint of the certificate",
|
||||
max_length=59,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="fingerprint_sha256",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="SHA256 fingerprint of the certificate",
|
||||
max_length=95,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="key_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("rsa", "RSA"),
|
||||
("ec", "Elliptic Curve"),
|
||||
("dsa", "DSA"),
|
||||
("ed25519", "Ed25519"),
|
||||
("ed448", "Ed448"),
|
||||
],
|
||||
help_text="Key algorithm type detected from the certificate's public key",
|
||||
max_length=16,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="certificatekeypair",
|
||||
name="kid",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Key ID generated from private key", max_length=128, null=True
|
||||
),
|
||||
),
|
||||
migrations.RunPython(backfill_certificate_metadata, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,7 +1,8 @@
|
||||
"""authentik crypto models"""
|
||||
|
||||
from base64 import urlsafe_b64encode
|
||||
from binascii import hexlify
|
||||
from hashlib import md5
|
||||
from hashlib import md5, sha512
|
||||
from ssl import PEM_FOOTER, PEM_HEADER
|
||||
from textwrap import wrap
|
||||
from uuid import uuid4
|
||||
@@ -47,6 +48,39 @@ def fingerprint_sha256(cert: Certificate) -> str:
|
||||
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
|
||||
|
||||
|
||||
def detect_key_type(certificate: Certificate) -> str | None:
|
||||
"""Detect the key algorithm type by parsing the certificate's public key"""
|
||||
try:
|
||||
public_key = certificate.public_key()
|
||||
if isinstance(public_key, RSAPublicKey):
|
||||
return KeyType.RSA
|
||||
if isinstance(public_key, EllipticCurvePublicKey):
|
||||
return KeyType.EC
|
||||
if isinstance(public_key, DSAPublicKey):
|
||||
return KeyType.DSA
|
||||
if isinstance(public_key, Ed25519PublicKey):
|
||||
return KeyType.ED25519
|
||||
if isinstance(public_key, Ed448PublicKey):
|
||||
return KeyType.ED448
|
||||
except (ValueError, TypeError, AttributeError) as exc:
|
||||
LOGGER.warning("Failed to detect key type", exc=exc)
|
||||
return None
|
||||
|
||||
|
||||
def generate_key_id(key_data: str) -> str:
|
||||
"""Generate Key ID using SHA512 + urlsafe_b64encode."""
|
||||
if not key_data:
|
||||
return ""
|
||||
return urlsafe_b64encode(sha512(key_data.encode("utf-8")).digest()).decode("utf-8").rstrip("=")
|
||||
|
||||
|
||||
def generate_key_id_legacy(key_data: str) -> str:
|
||||
"""Generate Key ID using MD5 (legacy format for backwards compatibility)."""
|
||||
if not key_data:
|
||||
return ""
|
||||
return md5(key_data.encode("utf-8")).hexdigest() # nosec
|
||||
|
||||
|
||||
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
|
||||
is set, otherwise it can be used to verify remote data."""
|
||||
@@ -62,6 +96,41 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
key_type = models.CharField(
|
||||
max_length=16,
|
||||
choices=KeyType.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Key algorithm type detected from the certificate's public key"),
|
||||
)
|
||||
cert_expiry = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Certificate expiry date"),
|
||||
)
|
||||
cert_subject = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Certificate subject as RFC4514 string"),
|
||||
)
|
||||
fingerprint_sha256 = models.CharField(
|
||||
max_length=95,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("SHA256 fingerprint of the certificate"),
|
||||
)
|
||||
fingerprint_sha1 = models.CharField(
|
||||
max_length=59,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("SHA1 fingerprint of the certificate"),
|
||||
)
|
||||
kid = models.CharField(
|
||||
max_length=128,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Key ID generated from private key"),
|
||||
)
|
||||
|
||||
_cert: Certificate | None = None
|
||||
_private_key: PrivateKeyTypes | None = None
|
||||
@@ -106,41 +175,6 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
return None
|
||||
return self._private_key
|
||||
|
||||
@property
|
||||
def fingerprint_sha256(self) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate_data"""
|
||||
return fingerprint_sha256(self.certificate)
|
||||
|
||||
@property
|
||||
def fingerprint_sha1(self) -> str:
|
||||
"""Get SHA1 Fingerprint of certificate_data"""
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA1()), ":").decode("utf-8") # nosec
|
||||
|
||||
@property
|
||||
def kid(self):
|
||||
"""Get Key ID used for JWKS"""
|
||||
return (
|
||||
md5(self.key_data.encode("utf-8"), usedforsecurity=False).hexdigest()
|
||||
if self.key_data
|
||||
else ""
|
||||
) # nosec
|
||||
|
||||
@property
|
||||
def key_type(self) -> str | None:
|
||||
"""Get the key algorithm type from the certificate's public key"""
|
||||
public_key = self.certificate.public_key()
|
||||
if isinstance(public_key, RSAPublicKey):
|
||||
return KeyType.RSA
|
||||
if isinstance(public_key, EllipticCurvePublicKey):
|
||||
return KeyType.EC
|
||||
if isinstance(public_key, DSAPublicKey):
|
||||
return KeyType.DSA
|
||||
if isinstance(public_key, Ed25519PublicKey):
|
||||
return KeyType.ED25519
|
||||
if isinstance(public_key, Ed448PublicKey):
|
||||
return KeyType.ED448
|
||||
return None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Certificate-Key Pair {self.name}"
|
||||
|
||||
|
||||
70
authentik/crypto/signals.py
Normal file
70
authentik/crypto/signals.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""authentik crypto signals"""
|
||||
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from ssl import CertificateError
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.x509 import Certificate
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.crypto.models import (
|
||||
CertificateKeyPair,
|
||||
detect_key_type,
|
||||
fingerprint_sha256,
|
||||
generate_key_id,
|
||||
generate_key_id_legacy,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def extract_certificate_metadata(certificate: Certificate) -> dict[str, str | datetime]:
|
||||
"""Extract all metadata fields from a certificate."""
|
||||
metadata = {}
|
||||
|
||||
try:
|
||||
metadata["key_type"] = detect_key_type(certificate)
|
||||
metadata["cert_expiry"] = certificate.not_valid_after_utc
|
||||
metadata["cert_subject"] = certificate.subject.rfc4514_string()
|
||||
metadata["fingerprint_sha256"] = fingerprint_sha256(certificate)
|
||||
metadata["fingerprint_sha1"] = hexlify(
|
||||
certificate.fingerprint(hashes.SHA1()), ":" # nosec
|
||||
).decode("utf-8")
|
||||
except (ValueError, TypeError, AttributeError) as exc:
|
||||
raise CertificateError(f"Invalid certificate metadata: {exc}") from exc
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
@receiver(pre_save, sender="authentik_crypto.CertificateKeyPair")
|
||||
def certificate_key_pair_pre_save(
|
||||
sender: type[CertificateKeyPair], instance: CertificateKeyPair, **_
|
||||
):
|
||||
"""Automatically populate certificate metadata fields before saving"""
|
||||
|
||||
# Only extract metadata if certificate_data is present
|
||||
if not instance.certificate_data:
|
||||
return
|
||||
|
||||
try:
|
||||
metadata = extract_certificate_metadata(instance.certificate)
|
||||
except (CertificateError, ValueError, TypeError, AttributeError) as exc:
|
||||
LOGGER.warning("Failed to extract certificate metadata", exc=exc)
|
||||
return
|
||||
|
||||
instance.key_type = metadata["key_type"]
|
||||
instance.cert_expiry = metadata["cert_expiry"]
|
||||
instance.cert_subject = metadata["cert_subject"]
|
||||
instance.fingerprint_sha256 = metadata["fingerprint_sha256"]
|
||||
instance.fingerprint_sha1 = metadata["fingerprint_sha1"]
|
||||
|
||||
# Generate kid if not set, or regenerate if key_data has changed
|
||||
# Preserve existing kid (MD5 or SHA512) if it matches the current key_data
|
||||
if instance.key_data:
|
||||
new_kid = generate_key_id(instance.key_data)
|
||||
legacy_kid = generate_key_id_legacy(instance.key_data)
|
||||
if instance.kid not in (new_kid, legacy_kid):
|
||||
instance.kid = new_kid
|
||||
@@ -20,7 +20,7 @@ from authentik.core.tests.utils import (
|
||||
)
|
||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.crypto.models import CertificateKeyPair, generate_key_id, generate_key_id_legacy
|
||||
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
@@ -173,21 +173,24 @@ class TestCrypto(APITestCase):
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
|
||||
self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256)
|
||||
|
||||
def test_list_without_details(self):
|
||||
"""Test API List (no details)"""
|
||||
def test_list_always_includes_details(self):
|
||||
"""Test API List always includes certificate details"""
|
||||
cert = create_test_cert()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-list",
|
||||
),
|
||||
data={"name": cert.name, "include_details": False},
|
||||
data={"name": cert.name},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], None)
|
||||
self.assertEqual(api_cert["fingerprint_sha256"], None)
|
||||
# All details should now always be included
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
|
||||
self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256)
|
||||
self.assertIsNotNone(api_cert["cert_expiry"])
|
||||
self.assertIsNotNone(api_cert["cert_subject"])
|
||||
|
||||
def test_certificate_download(self):
|
||||
"""Test certificate export (download)"""
|
||||
@@ -426,3 +429,114 @@ class TestCrypto(APITestCase):
|
||||
self.assertEqual(
|
||||
1, final_count, "Should not create duplicate cert for same private key"
|
||||
)
|
||||
|
||||
def test_metadata_extraction_with_cert_and_key(self):
|
||||
"""Test that metadata is extracted when creating keypair with certificate and key"""
|
||||
cert = create_test_cert()
|
||||
|
||||
# Verify all metadata fields are populated
|
||||
self.assertIsNotNone(cert.key_type)
|
||||
self.assertIsNotNone(cert.cert_expiry)
|
||||
self.assertIsNotNone(cert.cert_subject)
|
||||
self.assertIsNotNone(cert.fingerprint_sha256)
|
||||
self.assertIsNotNone(cert.fingerprint_sha1)
|
||||
|
||||
# Verify kid is generated using SHA512 for new records
|
||||
self.assertIsNotNone(cert.kid)
|
||||
self.assertEqual(cert.kid, generate_key_id(cert.key_data))
|
||||
|
||||
def test_metadata_extraction_without_key(self):
|
||||
"""Test that metadata is extracted when creating keypair without private key"""
|
||||
builder = CertificateBuilder(generate_id())
|
||||
builder.build(subject_alt_names=[], validity_days=3)
|
||||
|
||||
# Create keypair with only certificate, no key
|
||||
cert = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data=builder.certificate,
|
||||
key_data="",
|
||||
)
|
||||
|
||||
# Verify certificate metadata fields are populated
|
||||
self.assertIsNotNone(cert.key_type)
|
||||
self.assertIsNotNone(cert.cert_expiry)
|
||||
self.assertIsNotNone(cert.cert_subject)
|
||||
self.assertIsNotNone(cert.fingerprint_sha256)
|
||||
self.assertIsNotNone(cert.fingerprint_sha1)
|
||||
|
||||
# Verify kid is empty when no key_data
|
||||
self.assertEqual(cert.kid, None)
|
||||
|
||||
def test_metadata_extraction_invalid_cert(self):
|
||||
"""Test that invalid certificate data doesn't crash, just skips metadata"""
|
||||
cert = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data="invalid certificate data",
|
||||
key_data="",
|
||||
)
|
||||
|
||||
# Verify metadata fields are None for invalid cert
|
||||
self.assertIsNone(cert.key_type)
|
||||
self.assertIsNone(cert.cert_expiry)
|
||||
self.assertIsNone(cert.cert_subject)
|
||||
self.assertIsNone(cert.fingerprint_sha256)
|
||||
self.assertIsNone(cert.fingerprint_sha1)
|
||||
self.assertIsNone(cert.kid)
|
||||
|
||||
def test_kid_legacy_preservation(self):
|
||||
"""Test that legacy MD5 kid is preserved when key_data hasn't changed"""
|
||||
cert = create_test_cert()
|
||||
|
||||
# Simulate a legacy MD5 kid (as if backfilled from old system)
|
||||
legacy_kid = generate_key_id_legacy(cert.key_data)
|
||||
CertificateKeyPair.objects.filter(pk=cert.pk).update(kid=legacy_kid)
|
||||
cert.refresh_from_db()
|
||||
self.assertEqual(cert.kid, legacy_kid)
|
||||
|
||||
# Save the cert again (e.g., name change) - kid should be preserved
|
||||
cert.name = generate_id()
|
||||
cert.save()
|
||||
cert.refresh_from_db()
|
||||
|
||||
self.assertEqual(cert.kid, legacy_kid)
|
||||
|
||||
def test_kid_regenerated_on_key_change(self):
|
||||
"""Test that kid is regenerated when key_data changes"""
|
||||
cert = create_test_cert()
|
||||
original_kid = cert.kid
|
||||
|
||||
# Generate a new key and update the keypair
|
||||
builder = CertificateBuilder(generate_id())
|
||||
builder.build(subject_alt_names=[], validity_days=3)
|
||||
|
||||
cert.key_data = builder.private_key
|
||||
cert.certificate_data = builder.certificate
|
||||
cert.save()
|
||||
cert.refresh_from_db()
|
||||
|
||||
# Kid should be regenerated for the new key
|
||||
self.assertNotEqual(cert.kid, original_kid)
|
||||
self.assertEqual(cert.kid, generate_key_id(cert.key_data))
|
||||
|
||||
def test_kid_regenerated_on_key_change_from_legacy(self):
|
||||
"""Test that kid is regenerated from legacy MD5 when key_data changes"""
|
||||
cert = create_test_cert()
|
||||
|
||||
# Simulate a legacy MD5 kid
|
||||
legacy_kid = generate_key_id_legacy(cert.key_data)
|
||||
CertificateKeyPair.objects.filter(pk=cert.pk).update(kid=legacy_kid)
|
||||
cert.refresh_from_db()
|
||||
self.assertEqual(cert.kid, legacy_kid)
|
||||
|
||||
# Generate a new key and update the keypair
|
||||
builder = CertificateBuilder(generate_id())
|
||||
builder.build(subject_alt_names=[], validity_days=3)
|
||||
|
||||
cert.key_data = builder.private_key
|
||||
cert.certificate_data = builder.certificate
|
||||
cert.save()
|
||||
cert.refresh_from_db()
|
||||
|
||||
# Kid should now be SHA512 for the new key
|
||||
self.assertNotEqual(cert.kid, legacy_kid)
|
||||
self.assertEqual(cert.kid, generate_key_id(cert.key_data))
|
||||
|
||||
18
authentik/flows/auth.py
Normal file
18
authentik/flows/auth.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import cast
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.request import Request
|
||||
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
|
||||
|
||||
class FlowActive(BaseAuthentication):
|
||||
"""Authenticate requests when a flow is currently active"""
|
||||
|
||||
def authenticate(self, request: Request):
|
||||
plan = cast(FlowPlan | None, request.session.get(SESSION_KEY_PLAN))
|
||||
if not plan:
|
||||
return None
|
||||
return (plan.context.get(PLAN_CONTEXT_PENDING_USER, AnonymousUser()), plan)
|
||||
@@ -249,7 +249,7 @@ class ChallengeStageView(StageView):
|
||||
"f(ch): invalid challenge response",
|
||||
errors=challenge_response.errors,
|
||||
)
|
||||
return HttpChallengeResponse(challenge_response)
|
||||
return HttpChallengeResponse(challenge_response, status=400)
|
||||
|
||||
|
||||
class AccessDeniedStage(ChallengeStageView):
|
||||
|
||||
@@ -48,6 +48,9 @@ class FlowTestCase(APITestCase):
|
||||
self.assertEqual(raw_response[key], expected)
|
||||
return raw_response
|
||||
|
||||
def get_flow_plan(self) -> FlowPlan | None:
|
||||
return self.client.session.get(SESSION_KEY_PLAN)
|
||||
|
||||
def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]:
|
||||
"""Wrapper around assertStageResponse that checks for a redirect"""
|
||||
return self.assertStageResponse(response, component="xak-flow-redirect", to=to)
|
||||
|
||||
@@ -147,6 +147,8 @@ class FlowExecutorView(APIView):
|
||||
token.delete()
|
||||
if not isinstance(plan, FlowPlan):
|
||||
return None
|
||||
if existing_plan := self.request.session[SESSION_KEY_PLAN]:
|
||||
plan.context.update(existing_plan.context)
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
|
||||
return plan
|
||||
@@ -256,6 +258,11 @@ class FlowExecutorView(APIView):
|
||||
serializers=challenge_types,
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
400: PolymorphicProxySerializer(
|
||||
component_name="ChallengeTypes",
|
||||
serializers=challenge_types,
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
},
|
||||
request=OpenApiTypes.NONE,
|
||||
parameters=[
|
||||
@@ -303,6 +310,11 @@ class FlowExecutorView(APIView):
|
||||
serializers=challenge_types,
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
400: PolymorphicProxySerializer(
|
||||
component_name="ChallengeTypes",
|
||||
serializers=challenge_types,
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
},
|
||||
request=PolymorphicProxySerializer(
|
||||
component_name="FlowChallengeResponse",
|
||||
|
||||
@@ -86,7 +86,7 @@ class OutpostConfig:
|
||||
class OutpostModel(Model):
|
||||
"""Base model for providers that need more objects than just themselves"""
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
"""Return a list of all required objects"""
|
||||
return [self]
|
||||
|
||||
@@ -332,41 +332,35 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
|
||||
"""Create per-object and global permissions for outpost service-account"""
|
||||
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
||||
# the ones the user needs
|
||||
with transaction.atomic():
|
||||
user.remove_all_perms_from_managed_role()
|
||||
for model_or_perm in self.get_required_objects():
|
||||
if isinstance(model_or_perm, models.Model):
|
||||
model_or_perm: models.Model
|
||||
code_name = (
|
||||
f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}"
|
||||
)
|
||||
try:
|
||||
user.assign_perms_to_managed_role(code_name, model_or_perm)
|
||||
except (Permission.DoesNotExist, AttributeError) as exc:
|
||||
LOGGER.warning(
|
||||
"permission doesn't exist",
|
||||
code_name=code_name,
|
||||
user=user,
|
||||
model=model_or_perm,
|
||||
try:
|
||||
with transaction.atomic():
|
||||
user.remove_all_perms_from_managed_role()
|
||||
for model_or_perm in self.get_required_objects():
|
||||
if isinstance(model_or_perm, models.Model):
|
||||
code_name = (
|
||||
f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}"
|
||||
)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=(
|
||||
"While setting the permissions for the service-account, a "
|
||||
"permission was not found: Check "
|
||||
"https://docs.goauthentik.io/troubleshooting/missing_permission"
|
||||
),
|
||||
).with_exception(exc).set_user(user).save()
|
||||
else:
|
||||
app_label, perm = model_or_perm.split(".")
|
||||
permission = Permission.objects.filter(
|
||||
codename=perm,
|
||||
content_type__app_label=app_label,
|
||||
)
|
||||
if not permission.exists():
|
||||
LOGGER.warning("permission doesn't exist", perm=model_or_perm)
|
||||
continue
|
||||
user.assign_perms_to_managed_role(permission.first())
|
||||
user.assign_perms_to_managed_role(code_name, model_or_perm)
|
||||
elif isinstance(model_or_perm, tuple):
|
||||
perm, obj = model_or_perm
|
||||
user.assign_perms_to_managed_role(perm, obj)
|
||||
else:
|
||||
user.assign_perms_to_managed_role(model_or_perm)
|
||||
except (Permission.DoesNotExist, AttributeError) as exc:
|
||||
LOGGER.warning(
|
||||
"permission doesn't exist",
|
||||
code_name=code_name,
|
||||
user=user,
|
||||
model=model_or_perm,
|
||||
)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=(
|
||||
"While setting the permissions for the service-account, a "
|
||||
"permission was not found: Check "
|
||||
"https://docs.goauthentik.io/troubleshooting/missing_permission"
|
||||
),
|
||||
).with_exception(exc).set_user(user).save()
|
||||
LOGGER.debug(
|
||||
"Updated service account's permissions",
|
||||
obj_perms=user.get_all_obj_perms_on_managed_role(),
|
||||
@@ -431,7 +425,7 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
|
||||
Token.objects.filter(identifier=self.token_identifier).delete()
|
||||
return self.token
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
"""Get an iterator of all objects the user needs read access to"""
|
||||
objects: list[models.Model | str] = [
|
||||
self,
|
||||
@@ -445,7 +439,9 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
|
||||
if self.managed:
|
||||
for brand in Brand.objects.filter(web_certificate__isnull=False):
|
||||
objects.append(brand)
|
||||
objects.append(brand.web_certificate)
|
||||
objects.append(("view_certificatekeypair", brand.web_certificate))
|
||||
objects.append(("view_certificatekeypair_certificate", brand.web_certificate))
|
||||
objects.append(("view_certificatekeypair_key", brand.web_certificate))
|
||||
return objects
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -51,10 +51,12 @@ class OutpostTests(TestCase):
|
||||
permissions = outpost.user.get_all_obj_perms_on_managed_role().order_by(
|
||||
"content_type__model"
|
||||
)
|
||||
self.assertEqual(len(permissions), 3)
|
||||
self.assertEqual(len(permissions), 5)
|
||||
self.assertEqual(permissions[0].object_pk, str(keypair.pk))
|
||||
self.assertEqual(permissions[1].object_pk, str(outpost.pk))
|
||||
self.assertEqual(permissions[2].object_pk, str(provider.pk))
|
||||
self.assertEqual(permissions[1].object_pk, str(keypair.pk))
|
||||
self.assertEqual(permissions[2].object_pk, str(keypair.pk))
|
||||
self.assertEqual(permissions[3].object_pk, str(outpost.pk))
|
||||
self.assertEqual(permissions[4].object_pk, str(provider.pk))
|
||||
|
||||
# Remove provider from outpost, user should only have access to outpost
|
||||
outpost.providers.remove(provider)
|
||||
|
||||
@@ -93,11 +93,13 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
def __str__(self):
|
||||
return f"LDAP Provider {self.name}"
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self, "authentik_core.view_user", "authentik_core.view_group"]
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self, "authentik_core.view_user", "authentik_core.view_group"]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("LDAP Provider")
|
||||
|
||||
@@ -179,11 +179,13 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
def __str__(self):
|
||||
return f"Proxy Provider {self.name}"
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self]
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Proxy Provider")
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""proxy provider tests"""
|
||||
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.oauth2.models import ClientTypes
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
|
||||
@@ -127,3 +131,55 @@ class ProxyProviderTests(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
provider: ProxyProvider = ProxyProvider.objects.get(name=name)
|
||||
self.assertEqual(provider.client_type, ClientTypes.CONFIDENTIAL)
|
||||
|
||||
def test_sa_fetch(self):
|
||||
"""Test fetching the outpost config as the service account"""
|
||||
outpost = Outpost.objects.create(name=generate_id(), type=OutpostType.PROXY)
|
||||
provider = ProxyProvider.objects.create(name=generate_id())
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
outpost.providers.add(provider)
|
||||
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:proxyprovideroutpost-list"),
|
||||
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
|
||||
)
|
||||
body = loads(res.content)
|
||||
self.assertEqual(body["pagination"]["count"], 1)
|
||||
|
||||
def test_sa_perms_cert(self):
|
||||
"""Test permissions to access a configured certificate"""
|
||||
cert = create_test_cert()
|
||||
outpost = Outpost.objects.create(name=generate_id(), type=OutpostType.PROXY)
|
||||
provider = ProxyProvider.objects.create(name=generate_id(), certificate=cert)
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
outpost.providers.add(provider)
|
||||
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:proxyprovideroutpost-list"),
|
||||
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
|
||||
)
|
||||
body = loads(res.content)
|
||||
self.assertEqual(body["pagination"]["count"], 1)
|
||||
cert_id = body["results"][0]["certificate"]
|
||||
self.assertEqual(cert_id, str(cert.pk))
|
||||
|
||||
res = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-certificate",
|
||||
kwargs={
|
||||
"pk": cert_id,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
# res = self.client.get(
|
||||
# reverse(
|
||||
# "authentik_api:certificatekeypair-view-private-key",
|
||||
# kwargs={
|
||||
# "pk": cert_id,
|
||||
# },
|
||||
# ),
|
||||
# HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
|
||||
# )
|
||||
# self.assertEqual(res.status_code, 200)
|
||||
|
||||
@@ -64,10 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
|
||||
return RadiusProviderSerializer
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required.append(self.certificate)
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ChoiceField, IntegerField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
@@ -21,9 +21,11 @@ from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.flows.auth import FlowActive
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
|
||||
from authentik.stages.authenticator_duo.stage import PLAN_CONTEXT_DUO_ENROLL
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -84,14 +86,20 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
),
|
||||
},
|
||||
)
|
||||
@action(methods=["POST"], detail=True, permission_classes=[IsAuthenticated])
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=True,
|
||||
authentication_classes=[FlowActive],
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
def enrollment_status(self, request: Request, pk: str) -> Response:
|
||||
"""Check enrollment status of user details in current session"""
|
||||
stage: AuthenticatorDuoStage = AuthenticatorDuoStage.objects.filter(pk=pk).first()
|
||||
if not stage:
|
||||
raise Http404
|
||||
client = stage.auth_client()
|
||||
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
|
||||
plan: FlowPlan = request.auth
|
||||
enroll = plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
|
||||
if not enroll:
|
||||
return Response(status=400)
|
||||
status = client.enroll_status(enroll["user_id"], enroll["activation_code"])
|
||||
|
||||
@@ -14,7 +14,7 @@ from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import InvalidStageError
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
|
||||
SESSION_KEY_DUO_ENROLL = "authentik/stages/authenticator_duo/enroll"
|
||||
PLAN_CONTEXT_DUO_ENROLL = "goauthentik.io/stages/authenticator_duo/enroll"
|
||||
|
||||
|
||||
class AuthenticatorDuoChallenge(WithUserInfoChallenge):
|
||||
@@ -50,14 +50,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
|
||||
user=user,
|
||||
).from_http(self.request, user)
|
||||
raise InvalidStageError(str(exc)) from exc
|
||||
self.request.session[SESSION_KEY_DUO_ENROLL] = enroll
|
||||
self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL] = enroll
|
||||
return enroll
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
stage: AuthenticatorDuoStage = self.executor.current_stage
|
||||
if SESSION_KEY_DUO_ENROLL not in self.request.session:
|
||||
if PLAN_CONTEXT_DUO_ENROLL not in self.executor.plan.context:
|
||||
self.duo_enroll()
|
||||
enroll = self.request.session[SESSION_KEY_DUO_ENROLL]
|
||||
enroll = self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL]
|
||||
return AuthenticatorDuoChallenge(
|
||||
data={
|
||||
"activation_barcode": enroll["activation_barcode"],
|
||||
@@ -69,14 +69,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
# Duo Challenge has already been validated
|
||||
stage: AuthenticatorDuoStage = self.executor.current_stage
|
||||
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
|
||||
enroll = self.executor.plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
|
||||
enroll_status = stage.auth_client().enroll_status(
|
||||
enroll["user_id"], enroll["activation_code"]
|
||||
)
|
||||
if enroll_status != "success":
|
||||
return self.executor.stage_invalid(f"Invalid enrollment status: {enroll_status}.")
|
||||
existing_device = DuoDevice.objects.filter(duo_user_id=enroll["user_id"]).first()
|
||||
self.request.session.pop(SESSION_KEY_DUO_ENROLL)
|
||||
self.executor.plan.context.pop(PLAN_CONTEXT_DUO_ENROLL)
|
||||
if not existing_device:
|
||||
DuoDevice.objects.create(
|
||||
name="Duo Authenticator",
|
||||
@@ -88,6 +88,3 @@ class AuthenticatorDuoStageView(ChallengeStageView):
|
||||
else:
|
||||
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def cleanup(self):
|
||||
self.request.session.pop(SESSION_KEY_DUO_ENROLL, None)
|
||||
|
||||
@@ -11,7 +11,6 @@ from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
|
||||
@@ -51,42 +50,6 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_api_enrollment(self):
|
||||
"""Test `enrollment_status`"""
|
||||
self.client.force_login(self.user)
|
||||
stage = AuthenticatorDuoStage.objects.create(
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_id(),
|
||||
api_hostname=generate_id(),
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-enrollment-status",
|
||||
kwargs={
|
||||
"pk": str(stage.pk),
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_DUO_ENROLL] = {"user_id": "foo", "activation_code": "bar"}
|
||||
session.save()
|
||||
|
||||
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="foo")):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-enrollment-status",
|
||||
kwargs={
|
||||
"pk": str(stage.pk),
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content.decode(), '{"duo_response":"foo"}')
|
||||
|
||||
def test_api_import_manual_invalid_username(self):
|
||||
"""Test `import_device_manual`"""
|
||||
self.client.force_login(self.user)
|
||||
@@ -314,6 +277,17 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
self.assertEqual(enroll_mock.call_count, 1)
|
||||
|
||||
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="success")):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-enrollment-status",
|
||||
kwargs={
|
||||
"pk": str(stage.pk),
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(response.content, {"duo_response": "success"})
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.http.request import QueryDict
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import BooleanField, CharField, IntegerField
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.challenge import (
|
||||
@@ -26,7 +26,7 @@ from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
SESSION_KEY_EMAIL_DEVICE = "authentik/stages/authenticator_email/email_device"
|
||||
PLAN_CONTEXT_EMAIL_DEVICE = "goauthentik.io/stages/authenticator_email/email_device"
|
||||
PLAN_CONTEXT_EMAIL = "email"
|
||||
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
|
||||
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
|
||||
@@ -47,7 +47,7 @@ class AuthenticatorEmailChallengeResponse(ChallengeResponse):
|
||||
|
||||
device: EmailDevice
|
||||
|
||||
code = IntegerField(required=False)
|
||||
code = CharField(required=False)
|
||||
email = CharField(required=False)
|
||||
|
||||
component = CharField(default="ak-stage-authenticator-email")
|
||||
@@ -79,7 +79,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
if EmailDevice.objects.filter(Q(email=email), stage=stage.pk).exists():
|
||||
raise ValidationError(_("Invalid email"))
|
||||
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
|
||||
try:
|
||||
message = TemplateEmailMessage(
|
||||
@@ -116,9 +116,9 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
self.logger.debug("got email from plan context")
|
||||
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_EMAIL)
|
||||
# Check device for email
|
||||
if SESSION_KEY_EMAIL_DEVICE in self.request.session:
|
||||
if PLAN_CONTEXT_EMAIL_DEVICE in self.executor.plan.context:
|
||||
self.logger.debug("got email from device in session")
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
if device.email == "":
|
||||
return None
|
||||
return device.email
|
||||
@@ -135,7 +135,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
|
||||
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
||||
response = super().get_response_instance(data)
|
||||
response.device = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
response.device = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
return response
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
@@ -147,11 +147,11 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
return self.executor.stage_invalid(
|
||||
_("The user already has an email address registered for MFA.")
|
||||
)
|
||||
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
|
||||
if PLAN_CONTEXT_EMAIL_DEVICE not in self.executor.plan.context:
|
||||
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
|
||||
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()
|
||||
device.generate_token(valid_secs=valid_secs, commit=False)
|
||||
self.request.session[SESSION_KEY_EMAIL_DEVICE] = device
|
||||
self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE] = device
|
||||
if email := self._has_email():
|
||||
device.email = email
|
||||
try:
|
||||
@@ -165,16 +165,16 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
|
||||
PLAN_CONTEXT_EMAIL, None
|
||||
)
|
||||
self.request.session.pop(SESSION_KEY_EMAIL_DEVICE, None)
|
||||
self.executor.plan.context.pop(PLAN_CONTEXT_EMAIL_DEVICE, None)
|
||||
self.logger.warning("failed to send email to pre-set address", exc=exc)
|
||||
return self.get(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
"""Email Token is validated by challenge"""
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
if not device.confirmed:
|
||||
return self.challenge_invalid(response)
|
||||
device.save()
|
||||
del self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
del self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
return self.executor.stage_ok()
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.template.exceptions import TemplateDoesNotExist
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.config import CONFIG
|
||||
@@ -21,9 +21,7 @@ from authentik.stages.authenticator_email.api import (
|
||||
EmailDeviceSerializer,
|
||||
)
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
|
||||
from authentik.stages.authenticator_email.stage import (
|
||||
SESSION_KEY_EMAIL_DEVICE,
|
||||
)
|
||||
from authentik.stages.authenticator_email.stage import PLAN_CONTEXT_EMAIL_DEVICE
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
@@ -33,7 +31,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.flow = create_test_flow()
|
||||
self.user = create_test_admin_user()
|
||||
self.user = create_test_user()
|
||||
self.user_noemail = create_test_user(email="")
|
||||
self.stage = AuthenticatorEmailStage.objects.create(
|
||||
name="email-authenticator",
|
||||
@@ -213,20 +211,26 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email"},
|
||||
)
|
||||
self.assertIn("email required", str(response.content))
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={"non_field_errors": [{"code": "invalid", "string": "email required"}]},
|
||||
)
|
||||
|
||||
# Test invalid code
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "code": "000000"},
|
||||
)
|
||||
self.assertIn("Code does not match", str(response.content))
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"code": "invalid", "string": "Code does not match"}]
|
||||
},
|
||||
)
|
||||
|
||||
# Test valid code
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
device = self.device
|
||||
token = device.token
|
||||
response = self.client.post(
|
||||
@@ -285,8 +289,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
|
||||
device = self.client.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
device = self.get_flow_plan().context[PLAN_CONTEXT_EMAIL_DEVICE]
|
||||
self.assertIsInstance(device, EmailDevice)
|
||||
self.assertFalse(device.confirmed)
|
||||
self.assertEqual(device.user, self.user)
|
||||
@@ -294,8 +297,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
# Test device confirmation and cleanup
|
||||
device.confirmed = True
|
||||
device.email = "new_test@authentik.local" # Use a different email
|
||||
self.client.session[SESSION_KEY_EMAIL_DEVICE] = device
|
||||
self.client.session.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "code": device.token},
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import BooleanField, CharField, IntegerField
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
@@ -20,7 +20,7 @@ from authentik.stages.authenticator_sms.models import (
|
||||
)
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
|
||||
PLAN_CONTEXT_SMS_DEVICE = "goauthentik.io/stages/authenticator_sms/sms_device"
|
||||
PLAN_CONTEXT_PHONE = "phone"
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
|
||||
|
||||
device: SMSDevice
|
||||
|
||||
code = IntegerField(required=False)
|
||||
code = CharField(required=False)
|
||||
phone_number = CharField(required=False)
|
||||
|
||||
component = CharField(default="ak-stage-authenticator-sms")
|
||||
@@ -70,7 +70,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
|
||||
raise ValidationError(_("Invalid phone number"))
|
||||
# No code yet, but we have a phone number, so send a verification message
|
||||
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
stage.send(self.request, device.token, device)
|
||||
|
||||
def _has_phone_number(self) -> str | None:
|
||||
@@ -78,9 +78,9 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):
|
||||
self.logger.debug("got phone number from plan context")
|
||||
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)
|
||||
if SESSION_KEY_SMS_DEVICE in self.request.session:
|
||||
if PLAN_CONTEXT_SMS_DEVICE in self.executor.plan.context:
|
||||
self.logger.debug("got phone number from device in session")
|
||||
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
if device.phone_number == "":
|
||||
return None
|
||||
return device.phone_number
|
||||
@@ -95,7 +95,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
|
||||
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
||||
response = super().get_response_instance(data)
|
||||
response.device = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
response.device = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
return response
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
@@ -103,10 +103,10 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
|
||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||
|
||||
if SESSION_KEY_SMS_DEVICE not in self.request.session:
|
||||
if PLAN_CONTEXT_SMS_DEVICE not in self.executor.plan.context:
|
||||
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
|
||||
device.generate_token(commit=False)
|
||||
self.request.session[SESSION_KEY_SMS_DEVICE] = device
|
||||
self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE] = device
|
||||
if phone_number := self._has_phone_number():
|
||||
device.phone_number = phone_number
|
||||
try:
|
||||
@@ -120,14 +120,14 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
|
||||
PLAN_CONTEXT_PHONE, None
|
||||
)
|
||||
self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)
|
||||
self.executor.plan.context.pop(PLAN_CONTEXT_SMS_DEVICE, None)
|
||||
self.logger.warning("failed to send SMS message to pre-set number", exc=exc)
|
||||
return self.get(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
"""SMS Token is validated by challenge"""
|
||||
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
if not device.confirmed:
|
||||
return self.challenge_invalid(response)
|
||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||
@@ -135,5 +135,5 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
self.logger.debug("Hashing number on device")
|
||||
device.set_hashed_number()
|
||||
device.save()
|
||||
del self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
del self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
return self.executor.stage_ok()
|
||||
|
||||
@@ -18,7 +18,7 @@ from authentik.stages.authenticator_sms.models import (
|
||||
SMSProviders,
|
||||
hash_phone_number,
|
||||
)
|
||||
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, SESSION_KEY_SMS_DEVICE
|
||||
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, PLAN_CONTEXT_SMS_DEVICE
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class AuthenticatorSMSStageTests(FlowTestCase):
|
||||
self.assertEqual(mocker.call_count, 1)
|
||||
self.assertEqual(mocker.request_history[0].method, "POST")
|
||||
request_body = dict(parse_qsl(mocker.request_history[0].body))
|
||||
device: SMSDevice = self.client.session[SESSION_KEY_SMS_DEVICE]
|
||||
device: SMSDevice = self.get_flow_plan().context[PLAN_CONTEXT_SMS_DEVICE]
|
||||
self.assertEqual(
|
||||
request_body,
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ from urllib.parse import quote
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik.flows.challenge import (
|
||||
@@ -32,10 +32,10 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
|
||||
|
||||
device: TOTPDevice
|
||||
|
||||
code = IntegerField()
|
||||
code = CharField()
|
||||
component = CharField(default="ak-stage-authenticator-totp")
|
||||
|
||||
def validate_code(self, code: int) -> int:
|
||||
def validate_code(self, code: str) -> str:
|
||||
"""Validate totp code"""
|
||||
if not self.device:
|
||||
raise ValidationError(_("Code does not match"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""authentik consent stage"""
|
||||
|
||||
from hmac import compare_digest
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@@ -23,7 +24,7 @@ PLAN_CONTEXT_CONSENT = "consent"
|
||||
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
|
||||
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
|
||||
SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec
|
||||
PLAN_CONTEXT_CONSENT_TOKEN = "goauthentik.io/stages/consent/token" # nosec
|
||||
|
||||
|
||||
class ConsentPermissionSerializer(PassiveSerializer):
|
||||
@@ -50,7 +51,9 @@ class ConsentChallengeResponse(ChallengeResponse):
|
||||
token = CharField(required=True)
|
||||
|
||||
def validate_token(self, token: str):
|
||||
if token != self.stage.executor.request.session[SESSION_KEY_CONSENT_TOKEN]:
|
||||
if not compare_digest(
|
||||
token, self.stage.executor.plan.context.get(PLAN_CONTEXT_CONSENT_TOKEN, "")
|
||||
):
|
||||
raise ValidationError(_("Invalid consent token, re-showing prompt"))
|
||||
return token
|
||||
|
||||
@@ -62,7 +65,7 @@ class ConsentStageView(ChallengeStageView):
|
||||
|
||||
def get_challenge(self) -> Challenge:
|
||||
token = str(uuid4())
|
||||
self.request.session[SESSION_KEY_CONSENT_TOKEN] = token
|
||||
self.executor.plan.context[PLAN_CONTEXT_CONSENT_TOKEN] = token
|
||||
data = {
|
||||
"permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []),
|
||||
"additional_permissions": self.executor.plan.context.get(
|
||||
|
||||
@@ -19,7 +19,7 @@ from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConse
|
||||
from authentik.stages.consent.stage import (
|
||||
PLAN_CONTEXT_CONSENT_HEADER,
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||
SESSION_KEY_CONSENT_TOKEN,
|
||||
PLAN_CONTEXT_CONSENT_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
@@ -83,11 +83,10 @@ class TestConsentStage(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
session = self.client.session
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
{
|
||||
"token": session[SESSION_KEY_CONSENT_TOKEN],
|
||||
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -122,7 +121,7 @@ class TestConsentStage(FlowTestCase):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
{
|
||||
"token": session[SESSION_KEY_CONSENT_TOKEN],
|
||||
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -19,7 +19,7 @@ from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
|
||||
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TOKEN
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
|
||||
|
||||
@@ -174,7 +174,7 @@ class TestEmailStage(FlowTestCase):
|
||||
kwargs={"flow_slug": self.flow.slug},
|
||||
),
|
||||
data={
|
||||
"token": self.client.session[SESSION_KEY_CONSENT_TOKEN],
|
||||
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
@@ -245,7 +245,10 @@ class WorkerStatusMiddleware(Middleware):
|
||||
WorkerStatusMiddleware.keep(status)
|
||||
except DB_ERRORS: # pragma: no cover
|
||||
sleep(10)
|
||||
pass
|
||||
try:
|
||||
connections.close_all()
|
||||
except DB_ERRORS:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def keep(status: WorkerStatus):
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./media:/data/media
|
||||
- ./data:/data
|
||||
- ./custom-templates:/templates
|
||||
worker:
|
||||
command: worker
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./media:/data/media
|
||||
- ./data:/data
|
||||
- ./certs:/certs
|
||||
- ./custom-templates:/templates
|
||||
volumes:
|
||||
|
||||
2
go.mod
2
go.mod
@@ -32,7 +32,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025120.26
|
||||
goauthentik.io/api/v3 v3.2026020.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.19.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -214,8 +214,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
goauthentik.io/api/v3 v3.2025120.26 h1:2lTMtjCWtdOeQe7kwjpGUx39qUEpcxcxTirIqMvn0Os=
|
||||
goauthentik.io/api/v3 v3.2025120.26/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2026020.3 h1:CKtPyAQToPT2yF5odTTc+IfPLhYeVX9FbLMeVnFgZps=
|
||||
goauthentik.io/api/v3 v3.2026020.3/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
@@ -38,14 +38,14 @@ function check_if_root {
|
||||
chown -R authentik:authentik /media /certs "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
chmod ug+rwx /media
|
||||
chmod ug+rx /certs
|
||||
exec chpst -u authentik:$GROUP env HOME=/authentik PATH="$PATH" $1
|
||||
exec chpst -u authentik:$GROUP env HOME=/authentik $1
|
||||
}
|
||||
|
||||
function run_authentik {
|
||||
if [[ -x "$(command -v authentik)" ]]; then
|
||||
exec authentik $@
|
||||
else
|
||||
exec env GOFIPS140=latest CGO_ENABLED=1 go run -v ./cmd/server/ $@
|
||||
exec go run -v ./cmd/server/ $@
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
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.1034.0",
|
||||
"aws-cdk": "^2.1100.1",
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1034.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1034.0.tgz",
|
||||
"integrity": "sha512-YsIeXmMP/9eGml/eoPs64kHzNR0IVezzwuH0XrLOtUCjYNb80cmmjoCNsMn96u9rJOte1Yg3jitrHi1wTqXAqw==",
|
||||
"version": "2.1100.1",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.1.tgz",
|
||||
"integrity": "sha512-q2poFrQh90TK6eqeI0zznA8r1JkDI63WVOSqC7gFGo6qjQjAnvFk/utxHoNRgAC0RL0CLd19uCcHh3jfX9NiSg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1034.0",
|
||||
"aws-cdk": "^2.1100.1",
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-12 15:51+0000\n"
|
||||
"POT-Creation-Date: 2025-12-17 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"
|
||||
@@ -558,6 +558,30 @@ msgid ""
|
||||
"encryption."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Key algorithm type detected from the certificate's public key"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Certificate expiry date"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Certificate subject as RFC4514 string"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "SHA256 fingerprint of the certificate"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "SHA1 fingerprint of the certificate"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Key ID generated from private key"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Certificate-Key Pair"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -529,3 +529,7 @@ class _PostgresConsumer(Consumer):
|
||||
conn.close()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
try:
|
||||
connections.close_all()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,7 @@ COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -47,7 +47,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
|
||||
# Stage 3: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/radius ./cmd/radius
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
33
schema.yml
33
schema.yml
@@ -4728,11 +4728,6 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
description: Only return certificate-key pairs with keys
|
||||
- in: query
|
||||
name: include_details
|
||||
schema:
|
||||
type: boolean
|
||||
default: true
|
||||
- in: query
|
||||
name: key_type
|
||||
schema:
|
||||
@@ -33586,7 +33581,8 @@ components:
|
||||
minLength: 1
|
||||
default: ak-stage-authenticator-email
|
||||
code:
|
||||
type: integer
|
||||
type: string
|
||||
minLength: 1
|
||||
email:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -33833,7 +33829,8 @@ components:
|
||||
minLength: 1
|
||||
default: ak-stage-authenticator-sms
|
||||
code:
|
||||
type: integer
|
||||
type: string
|
||||
minLength: 1
|
||||
phone_number:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -34107,7 +34104,8 @@ components:
|
||||
minLength: 1
|
||||
default: ak-stage-authenticator-totp
|
||||
code:
|
||||
type: integer
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- code
|
||||
AuthenticatorTOTPStage:
|
||||
@@ -34992,25 +34990,25 @@ components:
|
||||
type: string
|
||||
fingerprint_sha256:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Get certificate Hash (SHA256)
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: SHA256 fingerprint of the certificate
|
||||
fingerprint_sha1:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Get certificate Hash (SHA1)
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: SHA1 fingerprint of the certificate
|
||||
cert_expiry:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Get certificate expiry
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: Certificate expiry date
|
||||
cert_subject:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Get certificate subject as full rfc4514
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: Certificate subject as RFC4514 string
|
||||
private_key_available:
|
||||
type: boolean
|
||||
description: Show if this keypair has a private key configured or not
|
||||
@@ -35018,8 +35016,9 @@ components:
|
||||
key_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/KeyTypeEnum'
|
||||
nullable: true
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: Key algorithm type detected from the certificate's public key
|
||||
certificate_download_url:
|
||||
type: string
|
||||
description: Get URL to download certificate
|
||||
|
||||
@@ -5,11 +5,11 @@ services:
|
||||
restart: never
|
||||
network_mode: none
|
||||
volumes:
|
||||
- ${LOCAL_PROJECT_DIR:-../../}:/local
|
||||
- ../../:/local
|
||||
|
||||
gen:
|
||||
image: docker.io/openapitools/openapi-generator-cli:v7.16.0
|
||||
restart: never
|
||||
network_mode: none
|
||||
volumes:
|
||||
- ${LOCAL_PROJECT_DIR:-../../}:/local
|
||||
- ../../:/local
|
||||
|
||||
6
scripts/generate_docker_compose.py
Normal file → Executable file
6
scripts/generate_docker_compose.py
Normal file → Executable file
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik import authentik_version
|
||||
@@ -42,7 +44,7 @@ base = {
|
||||
"image": authentik_image,
|
||||
"ports": ["${COMPOSE_PORT_HTTP:-9000}:9000", "${COMPOSE_PORT_HTTPS:-9443}:9443"],
|
||||
"restart": "unless-stopped",
|
||||
"volumes": ["./media:/data/media", "./custom-templates:/templates"],
|
||||
"volumes": ["./data:/data", "./custom-templates:/templates"],
|
||||
},
|
||||
"worker": {
|
||||
"command": "worker",
|
||||
@@ -62,7 +64,7 @@ base = {
|
||||
"user": "root",
|
||||
"volumes": [
|
||||
"/var/run/docker.sock:/var/run/docker.sock",
|
||||
"./media:/data/media",
|
||||
"./data:/data",
|
||||
"./certs:/certs",
|
||||
"./custom-templates:/templates",
|
||||
],
|
||||
|
||||
@@ -249,36 +249,60 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
Raises a clear test failure if the element isn't found, the text doesn't appear
|
||||
within `timeout` seconds, or the text is not valid JSON.
|
||||
"""
|
||||
use_body = context is None
|
||||
wait_timeout = timeout or self.wait_timeout
|
||||
|
||||
def get_context() -> WebElement:
|
||||
"""Get or refresh the context element."""
|
||||
if use_body:
|
||||
return self.driver.find_element(By.TAG_NAME, "body")
|
||||
return context
|
||||
|
||||
def get_text_safely() -> str:
|
||||
"""Get element text, re-finding element if stale."""
|
||||
for _ in range(5):
|
||||
try:
|
||||
return get_context().text.strip()
|
||||
except StaleElementReferenceException:
|
||||
sleep(0.5)
|
||||
return get_context().text.strip()
|
||||
|
||||
def get_inner_html_safely() -> str:
|
||||
"""Get innerHTML, re-finding element if stale."""
|
||||
for _ in range(5):
|
||||
try:
|
||||
return get_context().get_attribute("innerHTML") or ""
|
||||
except StaleElementReferenceException:
|
||||
sleep(0.5)
|
||||
return get_context().get_attribute("innerHTML") or ""
|
||||
|
||||
try:
|
||||
if context is None:
|
||||
context = self.driver.find_element(By.TAG_NAME, "body")
|
||||
get_context()
|
||||
except NoSuchElementException:
|
||||
self.fail(
|
||||
f"No element found (defaulted to <body>). Current URL: {self.driver.current_url}"
|
||||
)
|
||||
|
||||
wait_timeout = timeout or self.wait_timeout
|
||||
wait = WebDriverWait(context, wait_timeout)
|
||||
wait = WebDriverWait(self.driver, wait_timeout)
|
||||
|
||||
try:
|
||||
wait.until(lambda d: len(d.text.strip()) != 0)
|
||||
wait.until(lambda d: len(get_text_safely()) != 0)
|
||||
except TimeoutException:
|
||||
snippet = context.text.strip()[:500].replace("\n", " ")
|
||||
snippet = get_text_safely()[:500].replace("\n", " ")
|
||||
self.fail(
|
||||
f"Timed out waiting for element text to appear at {self.driver.current_url}. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
)
|
||||
|
||||
body_text = context.text.strip()
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
body_text = get_text_safely()
|
||||
inner_html = get_inner_html_safely()
|
||||
|
||||
if "redirecting" in inner_html.lower():
|
||||
try:
|
||||
wait.until(lambda d: "redirecting" not in d.get_attribute("innerHTML").lower())
|
||||
wait.until(lambda d: "redirecting" not in get_inner_html_safely().lower())
|
||||
except TimeoutException:
|
||||
snippet = context.text.strip()[:500].replace("\n", " ")
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
snippet = get_text_safely()[:500].replace("\n", " ")
|
||||
inner_html = get_inner_html_safely()
|
||||
|
||||
self.fail(
|
||||
f"Timed out waiting for redirect to finish at {self.driver.current_url}. "
|
||||
@@ -286,8 +310,8 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
f"{inner_html or '<empty>'}"
|
||||
)
|
||||
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
body_text = context.text.strip()
|
||||
inner_html = get_inner_html_safely()
|
||||
body_text = get_text_safely()
|
||||
|
||||
snippet = body_text[:500].replace("\n", " ")
|
||||
|
||||
|
||||
1447
web/package-lock.json
generated
1447
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -93,15 +93,15 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@floating-ui/dom": "^1.7.4",
|
||||
"@formatjs/intl-listformat": "^7.7.13",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1760614339",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.3.1",
|
||||
"@goauthentik/eslint-config": "^1.1.1",
|
||||
"@goauthentik/prettier-config": "^3.2.1",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.4.0",
|
||||
"@goauthentik/eslint-config": "^1.2.0",
|
||||
"@goauthentik/prettier-config": "^3.3.1",
|
||||
"@goauthentik/tsconfig": "^1.0.5",
|
||||
"@hcaptcha/types": "^1.1.0",
|
||||
"@lit/context": "^1.1.6",
|
||||
@@ -117,20 +117,20 @@
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sentry/browser": "^10.29.0",
|
||||
"@storybook/addon-docs": "^10.1.7",
|
||||
"@storybook/addon-links": "^10.1.7",
|
||||
"@storybook/web-components": "^10.1.7",
|
||||
"@storybook/web-components-vite": "^10.1.7",
|
||||
"@sentry/browser": "^10.31.0",
|
||||
"@storybook/addon-docs": "^10.1.10",
|
||||
"@storybook/addon-links": "^10.1.10",
|
||||
"@storybook/web-components": "^10.1.10",
|
||||
"@storybook/web-components-vite": "^10.1.10",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"@vitest/browser": "^4.0.15",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"@vitest/browser": "^4.0.16",
|
||||
"@vitest/browser-playwright": "^4.0.15",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -143,15 +143,15 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"dompurify": "^3.3.1",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.39.1",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-lit": "^2.1.1",
|
||||
"eslint-plugin-wc": "^3.0.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"globals": "^16.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^5.73.1",
|
||||
"knip": "^5.75.1",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
@@ -182,9 +182,9 @@
|
||||
"turnstile-types": "^1.2.3",
|
||||
"type-fest": "^5.3.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^7.2.7",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.15",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -194,10 +194,10 @@
|
||||
"@esbuild/darwin-arm64": "^0.27.0",
|
||||
"@esbuild/linux-arm64": "^0.27.0",
|
||||
"@esbuild/linux-x64": "^0.27.0",
|
||||
"@rollup/rollup-darwin-arm64": "^4.53.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
|
||||
"chromedriver": "^143.0.1"
|
||||
"@rollup/rollup-darwin-arm64": "^4.53.5",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.53.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.53.5",
|
||||
"chromedriver": "^143.0.2"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/tsconfig": "^1.0.5",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"semver": "^7.7.3",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-swc": "^0.4.0",
|
||||
"@swc/cli": "^0.7.9",
|
||||
"@swc/core": "^1.15.3",
|
||||
"@swc/core": "^1.15.6",
|
||||
"base64-js": "^1.5.1",
|
||||
"bootstrap": "^5.3.8",
|
||||
"formdata-polyfill": "^2025.11.0",
|
||||
"globby": "16.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"rollup": "^4.53.3",
|
||||
"rollup": "^4.53.5",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -67,24 +67,9 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
@property({ type: Boolean, attribute: "singleton" })
|
||||
public singleton = false;
|
||||
|
||||
/**
|
||||
* Set to `true` to include certificate details (fingerprints, expiry, certificate subject, key type)
|
||||
* in the API response.
|
||||
* Each returned certificate's PEM data must be parsed using cryptography library,
|
||||
* public keys extracted, and hashes computed. With large result sets, this can add a lot of time
|
||||
* to responses.
|
||||
* Only enable when you actually need the detailed fields displayed in the UI.
|
||||
* For simple certificate selection dropdowns, leave this as `false` (default).
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "include-details" })
|
||||
public includeDetails = false;
|
||||
|
||||
/**
|
||||
* When allowedKeyTypes is set, only certificates or keypairs with matching
|
||||
* key algorithms will be shown. Since certificates must be parsed to
|
||||
* extract algorithm details, an instance with many certificates may experience
|
||||
* long delays and server performance slowdowns. Avoid setting this field whenever possible.
|
||||
* key algorithms will be shown.
|
||||
* @attr
|
||||
* @example [KeyTypeEnum.Rsa, KeyTypeEnum.Ec]
|
||||
*/
|
||||
@@ -123,7 +108,6 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: !this.noKey,
|
||||
includeDetails: this.includeDetails,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
|
||||
@@ -45,9 +45,9 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
||||
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<CertificateKeyPair>> {
|
||||
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList(
|
||||
await this.defaultEndpointConfig(),
|
||||
);
|
||||
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
});
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
|
||||
@@ -77,7 +77,11 @@ export class AgentConnectorSetup extends AKElement {
|
||||
<p>${msg("Afterwards, select the enrollment token you want to use:")}</p>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col">
|
||||
<p>${msg("Then download the configuration to deploy the authentik Agent")}</p>
|
||||
<p>
|
||||
${msg(
|
||||
"Next, download the configuration to deploy the authentik Agent via MDM",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-l-grid">
|
||||
|
||||
@@ -77,10 +77,13 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentTok
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Device Group")} name="deviceGroup">
|
||||
<ak-form-element-horizontal label=${msg("Device Access Group")} name="deviceGroup">
|
||||
<ak-endpoints-device-group-search
|
||||
.group=${this.instance?.deviceGroup}
|
||||
></ak-endpoints-device-group-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Select a device access group to be added to upon enrollment.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="expiring">
|
||||
<label class="pf-c-switch">
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DeviceViewPage extends AKElement {
|
||||
? msg(str`Device ${this.device?.name}`)
|
||||
: msg("Loading device..."),
|
||||
description: this.device?.facts.data.os
|
||||
? this.device?.facts.data.os?.name + " " + this.device?.facts.data.os?.version
|
||||
? `${this.device?.facts.data.os?.name} ${this.device?.facts.data.os?.version}`
|
||||
: undefined,
|
||||
icon: "fa fa-laptop",
|
||||
});
|
||||
@@ -110,7 +110,7 @@ export class DeviceViewPage extends AKElement {
|
||||
?good=${this.device.facts.data.network?.firewallEnabled}
|
||||
></ak-status-label>`,
|
||||
],
|
||||
[msg("Group"), this.device.accessGroupObj?.name ?? "-"],
|
||||
[msg("Device access group"), this.device.accessGroupObj?.name ?? "-"],
|
||||
[
|
||||
msg("Actions"),
|
||||
html`<ak-forms-modal>
|
||||
@@ -162,13 +162,13 @@ export class DeviceViewPage extends AKElement {
|
||||
></ak-status-label>`,
|
||||
],
|
||||
[
|
||||
msg("Disk size"),
|
||||
msg("Primary disk size"),
|
||||
rootDisk?.capacityTotalBytes
|
||||
? getSize(rootDisk.capacityTotalBytes)
|
||||
: "-",
|
||||
],
|
||||
[
|
||||
msg("Disk usage"),
|
||||
msg("Primary disk usage"),
|
||||
rootDisk?.capacityTotalBytes && rootDisk.capacityUsedBytes
|
||||
? html`<progress
|
||||
value="${rootDisk.capacityUsedBytes}"
|
||||
|
||||
@@ -91,6 +91,20 @@ export class DataExportListPage extends TablePage<DataExport> {
|
||||
</div>
|
||||
</dl>`;
|
||||
}
|
||||
|
||||
protected renderEmpty(_inner?: TemplateResult): TemplateResult {
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state icon=${this.pageIcon}
|
||||
><span
|
||||
>${msg(
|
||||
html`To create a data export, navigate to
|
||||
<a href="#/identity/users">Directory > Users</a> or to
|
||||
<a href="#/events/log">Events > Logs</a>.`,
|
||||
)}</span
|
||||
>
|
||||
</ak-empty-state>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -16,18 +16,33 @@ import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
// Same regex is used in the backend as well
|
||||
const VALID_FILE_NAME_PATTERN = /^[a-zA-Z0-9._/-]+$/;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source
|
||||
// This is perfect for the "pattern" attribute
|
||||
const VALID_FILE_NAME_PATTERN_STRING = VALID_FILE_NAME_PATTERN.source;
|
||||
|
||||
// Note: browsers compile `pattern` using the new `v` RegExp flag (Unicode sets). Under `/v`,
|
||||
// both `/` and `-` must be escaped inside character classes.
|
||||
const VALID_FILE_NAME_PATTERN_STRING = "^[a-zA-Z0-9._\\/\\-]+$";
|
||||
|
||||
function assertValidFileName(fileName: string): void {
|
||||
if (!VALID_FILE_NAME_PATTERN.test(fileName)) {
|
||||
throw new Error(
|
||||
msg("Filename can only contain letters, numbers, dots, hyphens, and underscores"),
|
||||
msg(
|
||||
"Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileExtension(fileName: string): string {
|
||||
const lastDot = fileName.lastIndexOf(".");
|
||||
if (lastDot <= 0) return "";
|
||||
return fileName.slice(lastDot);
|
||||
}
|
||||
|
||||
function hasBasenameExtension(fileName: string): boolean {
|
||||
const baseName = fileName.split("/").pop() ?? fileName;
|
||||
const lastDot = baseName.lastIndexOf(".");
|
||||
return lastDot > 0;
|
||||
}
|
||||
|
||||
@customElement("ak-file-upload-form")
|
||||
export class FileUploadForm extends Form<Record<string, unknown>> {
|
||||
@property({ type: String, useDefault: true })
|
||||
@@ -57,36 +72,36 @@ export class FileUploadForm extends Form<Record<string, unknown>> {
|
||||
throw new PreventFormSubmit("Selected file not provided", this);
|
||||
}
|
||||
|
||||
assertValidFileName(this.selectedFile.name);
|
||||
|
||||
const api = new AdminApi(DEFAULT_CONFIG);
|
||||
const customName = typeof data.fileName === "string" ? data.fileName.trim() : "";
|
||||
const customName = typeof data.name === "string" ? data.name.trim() : "";
|
||||
|
||||
// If custom name provided, validate and append original extension
|
||||
// Only validate the original filename if no custom name is provided
|
||||
let finalName = this.selectedFile.name;
|
||||
if (customName) {
|
||||
assertValidFileName(customName);
|
||||
const ext = this.selectedFile.name.substring(this.selectedFile.name.lastIndexOf("."));
|
||||
finalName = customName + ext;
|
||||
const ext = getFileExtension(this.selectedFile.name);
|
||||
finalName =
|
||||
ext && !hasBasenameExtension(customName) ? `${customName}${ext}` : customName;
|
||||
} else {
|
||||
assertValidFileName(this.selectedFile.name);
|
||||
}
|
||||
|
||||
return api
|
||||
.adminFileCreate({
|
||||
file: this.selectedFile,
|
||||
name: finalName,
|
||||
usage: this.usage,
|
||||
})
|
||||
.then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg("File uploaded successfully"),
|
||||
});
|
||||
assertValidFileName(finalName);
|
||||
|
||||
this.reset();
|
||||
})
|
||||
.finally(() => {
|
||||
this.clearFileInput();
|
||||
});
|
||||
await api.adminFileCreate({
|
||||
file: this.selectedFile,
|
||||
name: finalName,
|
||||
usage: this.usage,
|
||||
});
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg("File uploaded successfully"),
|
||||
});
|
||||
|
||||
this.reset();
|
||||
this.clearFileInput();
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
@@ -101,7 +116,7 @@ export class FileUploadForm extends Form<Record<string, unknown>> {
|
||||
@change=${this.#fileChangeListener}
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("File Name")} name="fileName">
|
||||
<ak-form-element-horizontal label=${msg("File Name")} name="name">
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
|
||||
@@ -71,40 +71,45 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
|
||||
if (!this.modelPermissions) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Role")} name="role">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Role[]> => {
|
||||
const args: RbacRolesListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
|
||||
return roles.results;
|
||||
}}
|
||||
.renderElement=${(role: Role): string => {
|
||||
return role.name;
|
||||
}}
|
||||
.value=${(role: Role | undefined): string | undefined => {
|
||||
return role?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
${this.modelPermissions?.results
|
||||
.filter((perm) => {
|
||||
const [_app, model] = this.model?.split(".") || "";
|
||||
return perm.codename !== `add_${model}`;
|
||||
})
|
||||
.map((perm) => {
|
||||
return html`<ak-switch-input
|
||||
name="permissions.${perm.codename}"
|
||||
label=${perm.name}
|
||||
></ak-switch-input>`;
|
||||
})}
|
||||
</form>`;
|
||||
return html`<span
|
||||
>${msg(
|
||||
"Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.",
|
||||
)}</span
|
||||
>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Role")} name="role">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Role[]> => {
|
||||
const args: RbacRolesListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
|
||||
return roles.results;
|
||||
}}
|
||||
.renderElement=${(role: Role): string => {
|
||||
return role.name;
|
||||
}}
|
||||
.value=${(role: Role | undefined): string | undefined => {
|
||||
return role?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
${this.modelPermissions?.results
|
||||
.filter((perm) => {
|
||||
const [_app, model] = this.model?.split(".") || "";
|
||||
return perm.codename !== `add_${model}`;
|
||||
})
|
||||
.map((perm) => {
|
||||
return html`<ak-switch-input
|
||||
name="permissions.${perm.codename}"
|
||||
label=${perm.name}
|
||||
></ak-switch-input>`;
|
||||
})}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Assign")}</span>
|
||||
<span slot="header">${msg("Assign permission to role")}</span>
|
||||
<span slot="header">${msg("Assign object permissions to role")}</span>
|
||||
<ak-rbac-role-object-permission-form
|
||||
model=${ifDefined(this.model)}
|
||||
objectPk=${ifDefined(this.objectPk)}
|
||||
@@ -95,7 +95,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
|
||||
>
|
||||
</ak-rbac-role-object-permission-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Assign role permissions")}
|
||||
${msg("Assign Object Permission")}
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
}
|
||||
@@ -135,9 +135,9 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
|
||||
const assignedToModel = item.modelPermissions.some(
|
||||
(uperm) => uperm.codename === perm.codename,
|
||||
);
|
||||
const assignedToObject = item.objectPermissions.some(
|
||||
(uperm) => uperm.codename === perm.codename,
|
||||
);
|
||||
const assignedToObject = item.objectPermissions
|
||||
.filter((uperm) => uperm.objectPk === this.objectPk)
|
||||
.some((uperm) => uperm.codename === perm.codename);
|
||||
|
||||
let tooltip: string | null = null;
|
||||
if (assignedToModel && assignedToObject) {
|
||||
|
||||
@@ -51,6 +51,11 @@ export class NavigationButtons extends WithSession(AKElement) {
|
||||
Styles,
|
||||
];
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.refreshNotifications();
|
||||
}
|
||||
|
||||
protected async refreshNotifications(): Promise<void> {
|
||||
const { currentUser } = this;
|
||||
|
||||
|
||||
@@ -4183,9 +4183,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4284,12 +4281,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9550,9 +9541,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9685,9 +9673,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9901,6 +9886,41 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4214,9 +4214,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4315,12 +4312,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9590,9 +9581,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9725,9 +9713,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9941,6 +9926,41 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3220,9 +3220,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -3319,12 +3316,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -7383,9 +7374,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -7518,9 +7506,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -7734,6 +7719,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4151,9 +4151,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4252,12 +4249,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9510,9 +9501,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9645,9 +9633,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9861,6 +9846,41 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4293,9 +4293,6 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4394,12 +4391,6 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9773,9 +9764,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9908,9 +9896,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -10124,6 +10109,41 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4282,9 +4282,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4383,12 +4380,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9758,9 +9749,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9893,9 +9881,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -10109,6 +10094,41 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4110,9 +4110,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4211,12 +4208,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9458,9 +9449,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9593,9 +9581,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9809,6 +9794,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4284,9 +4284,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4385,12 +4382,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9752,9 +9743,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9887,9 +9875,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -10103,6 +10088,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3951,9 +3951,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4052,12 +4049,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9083,9 +9074,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9218,9 +9206,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9434,6 +9419,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3789,9 +3789,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -3890,12 +3887,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -8731,9 +8722,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -8866,9 +8854,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9082,6 +9067,41 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3966,9 +3966,6 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4067,12 +4064,6 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9113,9 +9104,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9248,9 +9236,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9464,6 +9449,41 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4287,9 +4287,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4388,12 +4385,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9741,9 +9732,6 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9876,9 +9864,6 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -10092,6 +10077,41 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4004,9 +4004,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4105,12 +4102,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9201,9 +9192,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9336,9 +9324,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9552,6 +9537,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3983,9 +3983,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4084,12 +4081,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9179,9 +9170,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9314,9 +9302,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9530,6 +9515,41 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4250,9 +4250,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -4351,12 +4348,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -9713,9 +9704,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -9848,9 +9836,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -10064,6 +10049,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -3821,9 +3821,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="sf96a86df0756bc7b">
|
||||
<source>Afterwards, select the enrollment token you want to use:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s13b4cf044a01dde2">
|
||||
<source>Then download the configuration to deploy the authentik Agent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12f523a52b843ea2">
|
||||
<source>macOS</source>
|
||||
</trans-unit>
|
||||
@@ -3922,12 +3919,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s94a50f1495fb5ccd">
|
||||
<source>Disk encryption</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76ea179414f2b2a5">
|
||||
<source>Disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25e7a078391a3ec3">
|
||||
<source>Disk usage</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s416211a967a6db4e">
|
||||
<source>Users / Groups</source>
|
||||
</trans-unit>
|
||||
@@ -8794,9 +8785,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s6df8326edea3b23d">
|
||||
<source>If no device was provided, this stage will stop flow execution.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506c7d2e87f6770e">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3a3d5b2575cd32ea">
|
||||
<source>File uploaded successfully</source>
|
||||
</trans-unit>
|
||||
@@ -8929,9 +8917,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc47f8ab6162bb2bb">
|
||||
<source>Outpost configuration</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s184c3b30bebb2dd8">
|
||||
<source>Assign role permissions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s11a2ac9f2bd811d8">
|
||||
<source>Delete Object Permission</source>
|
||||
</trans-unit>
|
||||
@@ -9145,6 +9130,41 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s46a03121a2c260ea">
|
||||
<source>Buffer PolicyAccessView requests</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s361e9d929ee925e6">
|
||||
<source>Assign Object Permission</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5f6ad947b4824e40">
|
||||
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s523337424b694d5c">
|
||||
<source>Device Access Group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7642bf28cf8f476c">
|
||||
<source>Select a device access group to be added to upon enrollment.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="h30bbf18fbf87fa57">
|
||||
<source>To create a data export, navigate to
|
||||
<x id="0" equiv-text="<a href="#/identity/users">"/>Directory > Users<x id="1" equiv-text="</a>"/> or to
|
||||
<x id="2" equiv-text="<a href="#/events/log">"/>Events > Logs<x id="3" equiv-text="</a>"/>.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s813faec5ff1d32d1">
|
||||
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s54af3ec70642782c">
|
||||
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3be65525dd1f92c">
|
||||
<source>Assign object permissions to role</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcd108b66363075c">
|
||||
<source>Device access group</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s325acae04cdcac57">
|
||||
<source>Primary disk size</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4a957846c89fca1">
|
||||
<source>Primary disk usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user