Compare commits
49 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
a2a67161ac | |||
2e8263a99b | |||
6b9afed21f | |||
1eb1f4e0b8 | |||
7c3d60ec3a | |||
a494c6b6e8 | |||
6604d3577f | |||
f8bfa7e16a | |||
ea6cf6eabf | |||
769ce3ce7b | |||
3891fb3fa8 | |||
41eb965350 | |||
8d95612287 | |||
82b5274b15 | |||
af56ce3d78 | |||
f5c6e7aeb0 | |||
3809400e93 | |||
1def9865cf | |||
3716298639 | |||
c16317d7cf | |||
bbb8fa8269 | |||
e4c251a178 | |||
0fefd5f522 | |||
88057db0b0 | |||
91cb6c9beb | |||
8e72fcab59 | |||
261879022d | |||
2a47ff2977 | |||
c3a81a1cce | |||
220d739fef | |||
4a57c6f230 | |||
4a93b97bec | |||
ac2bbd7e2f | |||
ad9f500ad1 | |||
15d7175750 | |||
41d372a340 | |||
83b84e8d26 | |||
f22daca091 | |||
ae4d5a30f2 | |||
9708481005 | |||
1c32c9e06d | |||
7a3d92ffdb | |||
a72b36d94d | |||
6b25f6f592 | |||
7d91842e8a | |||
2b4b1d2f76 | |||
2ce5c74f33 | |||
168fabfc70 | |||
eb53c28352 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2023.10.1
|
||||
current_version = 2023.10.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
16
.github/actions/setup/action.yml
vendored
16
.github/actions/setup/action.yml
vendored
@ -9,23 +9,27 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install poetry
|
||||
- name: Install poetry & deps
|
||||
shell: bash
|
||||
run: |
|
||||
pipx install poetry || true
|
||||
sudo apt update
|
||||
sudo apt install -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
|
||||
- name: Setup python and restore poetry
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version-file: 'pyproject.toml'
|
||||
cache: "poetry"
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
|
7
.github/workflows/ci-main.yml
vendored
7
.github/workflows/ci-main.yml
vendored
@ -11,6 +11,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
@ -185,6 +186,9 @@ jobs:
|
||||
build:
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -235,6 +239,9 @@ jobs:
|
||||
build-arm64:
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
6
.github/workflows/ci-outpost.yml
vendored
6
.github/workflows/ci-outpost.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-golint:
|
||||
@ -65,6 +66,9 @@ jobs:
|
||||
- ldap
|
||||
- radius
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@ -126,7 +130,7 @@ jobs:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Generate API
|
||||
|
11
.github/workflows/ci-web.yml
vendored
11
.github/workflows/ci-web.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-eslint:
|
||||
@ -23,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: ${{ matrix.project }}/package-lock.json
|
||||
- working-directory: ${{ matrix.project }}/
|
||||
@ -39,7 +40,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
@ -61,7 +62,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: ${{ matrix.project }}/package-lock.json
|
||||
- working-directory: ${{ matrix.project }}/
|
||||
@ -77,7 +78,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
@ -109,7 +110,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
|
7
.github/workflows/ci-website.yml
vendored
7
.github/workflows/ci-website.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- version-*
|
||||
|
||||
jobs:
|
||||
lint-prettier:
|
||||
@ -17,7 +18,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
@ -31,7 +32,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
@ -52,7 +53,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
|
1
.github/workflows/release-next-branch.yml
vendored
1
.github/workflows/release-next-branch.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Needed to be able to push to the next branch
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
11
.github/workflows/release-publish.yml
vendored
11
.github/workflows/release-publish.yml
vendored
@ -7,6 +7,9 @@ on:
|
||||
jobs:
|
||||
build-server:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
@ -52,6 +55,9 @@ jobs:
|
||||
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -106,6 +112,9 @@ jobs:
|
||||
build-outpost-binary:
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload binaries to the release
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -122,7 +131,7 @@ jobs:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
|
2
.github/workflows/repo-stale.yml
vendored
2
.github/workflows/repo-stale.yml
vendored
@ -6,8 +6,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Needed to update issues and PRs
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
2
.github/workflows/web-api-publish.yml
vendored
2
.github/workflows/web-api-publish.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Generate API Client
|
||||
run: make gen-client-ts
|
||||
|
13
Dockerfile
13
Dockerfile
@ -35,7 +35,14 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM docker.io/golang:1.21.3-bookworm AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -57,10 +64,10 @@ ENV CGO_ENABLED=0
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -o /go/authentik ./cmd/server
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server
|
||||
|
||||
# Stage 4: MaxMind GeoIP
|
||||
FROM ghcr.io/maxmind/geoipupdate:v6.0 as geoip
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip
|
||||
|
||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
|
||||
ENV GEOIPUPDATE_VERBOSE="true"
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.10.1"
|
||||
__version__ = "2023.10.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -584,12 +584,17 @@ class EntryInvalidError(SentryIgnoredException):
|
||||
entry_model: Optional[str]
|
||||
entry_id: Optional[str]
|
||||
validation_error: Optional[ValidationError]
|
||||
serializer: Optional[Serializer] = None
|
||||
|
||||
def __init__(self, *args: object, validation_error: Optional[ValidationError] = None) -> None:
|
||||
def __init__(
|
||||
self, *args: object, validation_error: Optional[ValidationError] = None, **kwargs
|
||||
) -> None:
|
||||
super().__init__(*args)
|
||||
self.entry_model = None
|
||||
self.entry_id = None
|
||||
self.validation_error = validation_error
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@staticmethod
|
||||
def from_entry(
|
||||
|
@ -255,7 +255,10 @@ class Importer:
|
||||
try:
|
||||
full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
|
||||
except ValueError as exc:
|
||||
raise EntryInvalidError.from_entry(exc, entry) from exc
|
||||
raise EntryInvalidError.from_entry(
|
||||
exc,
|
||||
entry,
|
||||
) from exc
|
||||
always_merger.merge(full_data, updated_identifiers)
|
||||
serializer_kwargs["data"] = full_data
|
||||
|
||||
@ -272,6 +275,7 @@ class Importer:
|
||||
f"Serializer errors {serializer.errors}",
|
||||
validation_error=exc,
|
||||
entry=entry,
|
||||
serializer=serializer,
|
||||
) from exc
|
||||
return serializer
|
||||
|
||||
@ -300,16 +304,18 @@ class Importer:
|
||||
)
|
||||
return False
|
||||
# Validate each single entry
|
||||
serializer = None
|
||||
try:
|
||||
serializer = self._validate_single(entry)
|
||||
except EntryInvalidError as exc:
|
||||
# For deleting objects we don't need the serializer to be valid
|
||||
if entry.get_state(self._import) == BlueprintEntryDesiredState.ABSENT:
|
||||
continue
|
||||
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
|
||||
if raise_errors:
|
||||
raise exc
|
||||
return False
|
||||
serializer = exc.serializer
|
||||
else:
|
||||
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
|
||||
if raise_errors:
|
||||
raise exc
|
||||
return False
|
||||
if not serializer:
|
||||
continue
|
||||
|
||||
|
@ -82,7 +82,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
path = Path(event.src_path)
|
||||
root = Path(CONFIG.get("blueprints_dir")).absolute()
|
||||
rel_path = str(path.relative_to(root))
|
||||
for instance in BlueprintInstance.objects.filter(path=rel_path):
|
||||
for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
|
||||
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
|
||||
apply_blueprint.delay(instance.pk.hex)
|
||||
|
||||
|
@ -171,6 +171,11 @@ class UserSerializer(ModelSerializer):
|
||||
raise ValidationError("Setting a user to internal service account is not allowed.")
|
||||
return user_type
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
raise ValidationError("Can't modify internal service account users")
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
|
@ -17,9 +17,15 @@ class Command(BaseCommand):
|
||||
"""Run worker"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-b", "--beat", action="store_true")
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--beat",
|
||||
action="store_false",
|
||||
help="When set, this worker will _not_ run Beat (scheduled) tasks",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
LOGGER.debug("Celery options", **options)
|
||||
close_old_connections()
|
||||
if CONFIG.get_bool("remote_debug"):
|
||||
import debugpy
|
||||
|
@ -1,13 +1,10 @@
|
||||
"""authentik crypto app config"""
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import Optional
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
|
||||
|
||||
|
||||
@ -23,33 +20,37 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||
"""Load crypto tasks"""
|
||||
self.import_module("authentik.crypto.tasks")
|
||||
|
||||
def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None):
|
||||
def _create_update_cert(self):
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
builder = CertificateBuilder("authentik Internal JWT Certificate")
|
||||
common_name = "authentik Internal JWT Certificate"
|
||||
builder = CertificateBuilder(common_name)
|
||||
builder.build(
|
||||
subject_alt_names=["goauthentik.io"],
|
||||
validity_days=360,
|
||||
)
|
||||
if not cert:
|
||||
cert = CertificateKeyPair()
|
||||
builder.cert = cert
|
||||
builder.cert.managed = MANAGED_KEY
|
||||
builder.save()
|
||||
CertificateKeyPair.objects.update_or_create(
|
||||
managed=MANAGED_KEY,
|
||||
defaults={
|
||||
"name": common_name,
|
||||
"certificate_data": builder.certificate,
|
||||
"key_data": builder.private_key,
|
||||
},
|
||||
)
|
||||
|
||||
def reconcile_managed_jwt_cert(self):
|
||||
"""Ensure managed JWT certificate"""
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
|
||||
if not certs.exists():
|
||||
self._create_update_cert()
|
||||
return
|
||||
cert: CertificateKeyPair = certs.first()
|
||||
cert: Optional[CertificateKeyPair] = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_KEY
|
||||
).first()
|
||||
now = datetime.now()
|
||||
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
|
||||
self._create_update_cert(cert)
|
||||
if not cert or (
|
||||
now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after
|
||||
):
|
||||
self._create_update_cert()
|
||||
|
||||
def reconcile_self_signed(self):
|
||||
"""Create self-signed keypair"""
|
||||
@ -61,4 +62,10 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||
return
|
||||
builder = CertificateBuilder(name)
|
||||
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
|
||||
builder.save()
|
||||
CertificateKeyPair.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
"certificate_data": builder.certificate,
|
||||
"key_data": builder.private_key,
|
||||
},
|
||||
)
|
||||
|
@ -27,6 +27,7 @@ from authentik.lib.sentry import before_send
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.outposts.models import OutpostServiceConnection
|
||||
from authentik.policies.models import Policy, PolicyBindingModel
|
||||
from authentik.policies.reputation.models import Reputation
|
||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
||||
from authentik.stages.authenticator_static.models import StaticToken
|
||||
@ -52,11 +53,13 @@ IGNORED_MODELS = (
|
||||
RefreshToken,
|
||||
SCIMUser,
|
||||
SCIMGroup,
|
||||
Reputation,
|
||||
)
|
||||
|
||||
|
||||
def should_log_model(model: Model) -> bool:
|
||||
"""Return true if operation on `model` should be logged"""
|
||||
# Check for silk by string so this comparison doesn't fail when silk isn't installed
|
||||
if model.__module__.startswith("silk"):
|
||||
return False
|
||||
return model.__class__ not in IGNORED_MODELS
|
||||
@ -93,21 +96,30 @@ class AuditMiddleware:
|
||||
of models"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
anonymous_user: User = None
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def _ensure_fallback_user(self):
|
||||
"""Defer fetching anonymous user until we have to"""
|
||||
if self.anonymous_user:
|
||||
return
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
self.anonymous_user = get_anonymous_user()
|
||||
|
||||
def connect(self, request: HttpRequest):
|
||||
"""Connect signal for automatic logging"""
|
||||
if not hasattr(request, "user"):
|
||||
return
|
||||
if not getattr(request.user, "is_authenticated", False):
|
||||
return
|
||||
self._ensure_fallback_user()
|
||||
user = getattr(request, "user", self.anonymous_user)
|
||||
if not user.is_authenticated:
|
||||
user = self.anonymous_user
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
|
||||
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
|
||||
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
|
||||
post_save_handler = partial(self.post_save_handler, user=user, request=request)
|
||||
pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
|
||||
m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
|
||||
post_save.connect(
|
||||
post_save_handler,
|
||||
dispatch_uid=request.request_id,
|
||||
|
@ -13,6 +13,7 @@ from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
@ -92,4 +93,5 @@ def event_post_save_notification(sender, instance: Event, **_):
|
||||
@receiver(pre_delete, sender=User)
|
||||
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
||||
"""If gdpr_compliance is enabled, remove all the user's events"""
|
||||
gdpr_cleanup.delay(instance.pk)
|
||||
if CONFIG.get_bool("gdpr_compliance", True):
|
||||
gdpr_cleanup.delay(instance.pk)
|
||||
|
@ -153,6 +153,12 @@ def sanitize_item(value: Any) -> Any:
|
||||
return value.isoformat()
|
||||
if isinstance(value, timedelta):
|
||||
return str(value.total_seconds())
|
||||
if callable(value):
|
||||
return {
|
||||
"type": "callable",
|
||||
"name": value.__name__,
|
||||
"module": value.__module__,
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
|
34
authentik/flows/migrations/0027_auto_20231028_1424.py
Normal file
34
authentik/flows/migrations/0027_auto_20231028_1424.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-28 14:24
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
users = User.objects.using(db_alias).exclude(username="akadmin")
|
||||
try:
|
||||
users = users.exclude(pk=get_anonymous_user().pk)
|
||||
# pylint: disable=broad-except
|
||||
except Exception: # nosec
|
||||
pass
|
||||
|
||||
if users.exists():
|
||||
Flow.objects.filter(slug="initial-setup").update(authentication="require_superuser")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_flows", "0026_alter_flow_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_oobe_flow_authentication),
|
||||
]
|
@ -344,12 +344,22 @@ class Outpost(SerializerModel, ManagedModel):
|
||||
user_created = False
|
||||
if not user:
|
||||
user: User = User.objects.create(username=self.user_identifier)
|
||||
user.set_unusable_password()
|
||||
user_created = True
|
||||
user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
user.name = f"Outpost {self.name} Service-Account"
|
||||
user.path = USER_PATH_OUTPOSTS
|
||||
user.save()
|
||||
attrs = {
|
||||
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
"name": f"Outpost {self.name} Service-Account",
|
||||
"path": USER_PATH_OUTPOSTS,
|
||||
}
|
||||
dirty = False
|
||||
for key, value in attrs.items():
|
||||
if getattr(user, key) != value:
|
||||
dirty = True
|
||||
setattr(user, key, value)
|
||||
if user.has_usable_password():
|
||||
user.set_unusable_password()
|
||||
dirty = True
|
||||
if dirty:
|
||||
user.save()
|
||||
if user_created:
|
||||
self.build_user_permissions(user)
|
||||
return user
|
||||
|
187
authentik/providers/oauth2/tests/test_token_pkce.py
Normal file
187
authentik/providers/oauth2/tests/test_token_pkce.py
Normal file
@ -0,0 +1,187 @@
|
||||
"""Test token view"""
|
||||
from base64 import b64encode, urlsafe_b64encode
|
||||
from hashlib import sha256
|
||||
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
|
||||
from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
|
||||
class TestTokenPKCE(OAuthTestCase):
|
||||
"""Test token view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.app = Application.objects.create(name=generate_id(), slug="test")
|
||||
|
||||
def test_pkce_missing_in_token(self):
|
||||
"""Test full with pkce"""
|
||||
flow = create_test_flow()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
challenge = generate_id()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
"component": "xak-flow-redirect",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": f"foo://localhost?code={code.code}&state={state}",
|
||||
},
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
# Missing the code_verifier here
|
||||
"redirect_uri": "foo://localhost",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{"error": "invalid_request", "error_description": "The request is otherwise malformed"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_pkce_correct_s256(self):
|
||||
"""Test full with pkce"""
|
||||
flow = create_test_flow()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
verifier = generate_id()
|
||||
challenge = (
|
||||
urlsafe_b64encode(sha256(verifier.encode("ascii")).digest())
|
||||
.decode("utf-8")
|
||||
.replace("=", "")
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
"component": "xak-flow-redirect",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": f"foo://localhost?code={code.code}&state={state}",
|
||||
},
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": "foo://localhost",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_pkce_correct_plain(self):
|
||||
"""Test full with pkce"""
|
||||
flow = create_test_flow()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
verifier = generate_id()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
"code_challenge": verifier,
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
"component": "xak-flow-redirect",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": f"foo://localhost?code={code.code}&state={state}",
|
||||
},
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": "foo://localhost",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
@ -188,6 +188,7 @@ def authenticate_provider(request: HttpRequest) -> Optional[OAuth2Provider]:
|
||||
if client_id != provider.client_id or client_secret != provider.client_secret:
|
||||
LOGGER.debug("(basic) Provider for basic auth does not exist")
|
||||
return None
|
||||
CTX_AUTH_VIA.set("oauth_client_secret")
|
||||
return provider
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode
|
||||
from sentry_sdk.hub import Hub
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import CTX_AUTH_VIA
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_EXPIRES,
|
||||
USER_ATTRIBUTE_GENERATED,
|
||||
@ -221,7 +222,10 @@ class TokenParams:
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
# Validate PKCE parameters.
|
||||
if self.code_verifier:
|
||||
if self.authorization_code.code_challenge:
|
||||
# Authorization code had PKCE but we didn't get one
|
||||
if not self.code_verifier:
|
||||
raise TokenError("invalid_request")
|
||||
if self.authorization_code.code_challenge_method == PKCE_METHOD_S256:
|
||||
new_code_challenge = (
|
||||
urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
|
||||
@ -448,6 +452,7 @@ class TokenView(View):
|
||||
if not self.provider:
|
||||
LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
|
||||
raise TokenError("invalid_client")
|
||||
CTX_AUTH_VIA.set("oauth_client_secret")
|
||||
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
|
||||
|
||||
with Hub.current.start_span(
|
||||
|
@ -46,7 +46,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
|
||||
def to_scim(self, obj: Group) -> SCIMGroupSchema:
|
||||
"""Convert authentik user into SCIM"""
|
||||
raw_scim_group = {}
|
||||
raw_scim_group = {
|
||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
|
||||
}
|
||||
for mapping in (
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
|
||||
):
|
||||
|
@ -15,12 +15,14 @@ from pydanticscim.user import User as BaseUser
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
|
||||
externalId: Optional[str] = None
|
||||
|
||||
|
||||
class Group(BaseGroup):
|
||||
"""Modified Group schema with added externalId field"""
|
||||
|
||||
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
|
||||
externalId: Optional[str] = None
|
||||
|
||||
|
||||
|
@ -39,7 +39,9 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
|
||||
|
||||
def to_scim(self, obj: User) -> SCIMUserSchema:
|
||||
"""Convert authentik user into SCIM"""
|
||||
raw_scim_user = {}
|
||||
raw_scim_user = {
|
||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
|
||||
}
|
||||
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
||||
if not isinstance(mapping, SCIMMapping):
|
||||
continue
|
||||
|
@ -61,7 +61,11 @@ class SCIMGroupTests(TestCase):
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{"externalId": str(group.pk), "displayName": group.name},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
|
||||
@Mocker()
|
||||
@ -96,7 +100,11 @@ class SCIMGroupTests(TestCase):
|
||||
validate(body, loads(schema.read()))
|
||||
self.assertEqual(
|
||||
body,
|
||||
{"externalId": str(group.pk), "displayName": group.name},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
group.save()
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
@ -129,7 +137,11 @@ class SCIMGroupTests(TestCase):
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{"externalId": str(group.pk), "displayName": group.name},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
group.delete()
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
|
@ -89,6 +89,7 @@ class SCIMMembershipTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[3].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"emails": [],
|
||||
"active": True,
|
||||
"externalId": user.uid,
|
||||
@ -99,7 +100,11 @@ class SCIMMembershipTests(TestCase):
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[5].body,
|
||||
{"externalId": str(group.pk), "displayName": group.name},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
|
||||
with Mocker() as mocker:
|
||||
@ -118,6 +123,7 @@ class SCIMMembershipTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "add",
|
||||
@ -125,7 +131,6 @@ class SCIMMembershipTests(TestCase):
|
||||
"value": [{"value": user_scim_id}],
|
||||
}
|
||||
],
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
},
|
||||
)
|
||||
|
||||
@ -174,6 +179,7 @@ class SCIMMembershipTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[3].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"displayName": "",
|
||||
"emails": [],
|
||||
@ -184,7 +190,11 @@ class SCIMMembershipTests(TestCase):
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[5].body,
|
||||
{"externalId": str(group.pk), "displayName": group.name},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
|
||||
with Mocker() as mocker:
|
||||
@ -203,6 +213,7 @@ class SCIMMembershipTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "add",
|
||||
@ -210,7 +221,6 @@ class SCIMMembershipTests(TestCase):
|
||||
"value": [{"value": user_scim_id}],
|
||||
}
|
||||
],
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
},
|
||||
)
|
||||
|
||||
@ -230,6 +240,7 @@ class SCIMMembershipTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "remove",
|
||||
@ -237,6 +248,5 @@ class SCIMMembershipTests(TestCase):
|
||||
"value": [{"value": user_scim_id}],
|
||||
}
|
||||
],
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
},
|
||||
)
|
||||
|
@ -66,6 +66,7 @@ class SCIMUserTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
@ -121,6 +122,7 @@ class SCIMUserTests(TestCase):
|
||||
self.assertEqual(
|
||||
body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
@ -173,6 +175,7 @@ class SCIMUserTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
@ -240,6 +243,7 @@ class SCIMUserTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
|
@ -32,13 +32,19 @@ class PermissionSerializer(ModelSerializer):
|
||||
|
||||
def get_app_label_verbose(self, instance: Permission) -> str:
|
||||
"""Human-readable app label"""
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
try:
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
except LookupError:
|
||||
return f"{instance.content_type.app_label}.{instance.content_type.model}"
|
||||
|
||||
def get_model_verbose(self, instance: Permission) -> str:
|
||||
"""Human-readable model name"""
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
try:
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
except LookupError:
|
||||
return f"{instance.content_type.app_label}.{instance.content_type.model}"
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
|
@ -28,9 +28,12 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
||||
|
||||
def get_model_verbose(self, instance: GroupObjectPermission) -> str:
|
||||
"""Get model label from permission's model"""
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
try:
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
except LookupError:
|
||||
return f"{instance.content_type.app_label}.{instance.content_type.model}"
|
||||
|
||||
def get_object_description(self, instance: GroupObjectPermission) -> Optional[str]:
|
||||
"""Get model description from attached model. This operation takes at least
|
||||
@ -38,7 +41,10 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
||||
view_ permission on the object"""
|
||||
app_label = instance.content_type.app_label
|
||||
model = instance.content_type.model
|
||||
model_class = apps.get_model(app_label, model)
|
||||
try:
|
||||
model_class = apps.get_model(app_label, model)
|
||||
except LookupError:
|
||||
return None
|
||||
objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class)
|
||||
obj = objects.first()
|
||||
if not obj:
|
||||
|
@ -28,9 +28,12 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
||||
|
||||
def get_model_verbose(self, instance: UserObjectPermission) -> str:
|
||||
"""Get model label from permission's model"""
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
try:
|
||||
return apps.get_model(
|
||||
instance.content_type.app_label, instance.content_type.model
|
||||
)._meta.verbose_name
|
||||
except LookupError:
|
||||
return f"{instance.content_type.app_label}.{instance.content_type.model}"
|
||||
|
||||
def get_object_description(self, instance: UserObjectPermission) -> Optional[str]:
|
||||
"""Get model description from attached model. This operation takes at least
|
||||
@ -38,7 +41,10 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
||||
view_ permission on the object"""
|
||||
app_label = instance.content_type.app_label
|
||||
model = instance.content_type.model
|
||||
model_class = apps.get_model(app_label, model)
|
||||
try:
|
||||
model_class = apps.get_model(app_label, model)
|
||||
except LookupError:
|
||||
return None
|
||||
objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class)
|
||||
obj = objects.first()
|
||||
if not obj:
|
||||
|
@ -12,8 +12,9 @@ class PatreonOAuthRedirect(OAuthRedirect):
|
||||
"""Patreon OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
|
||||
# https://docs.patreon.com/#scopes
|
||||
return {
|
||||
"scope": ["openid", "email", "profile"],
|
||||
"scope": ["identity", "identity[email]"],
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""authentik multi-stage authentication engine"""
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@ -52,17 +53,13 @@ class EmailStageView(ChallengeStageView):
|
||||
kwargs={"flow_slug": self.executor.flow.slug},
|
||||
)
|
||||
# Parse query string from current URL (full query string)
|
||||
query_params = QueryDict(self.request.META.get("QUERY_STRING", ""), mutable=True)
|
||||
# this view is only run within a flow executor, where we need to get the query string
|
||||
# from the query= parameter (double encoded); but for the redirect
|
||||
# we need to expand it since it'll go through the flow interface
|
||||
query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
|
||||
query_params.pop(QS_KEY_TOKEN, None)
|
||||
|
||||
# Check for nested query string used by flow executor, and remove any
|
||||
# kind of flow token from that
|
||||
if QS_QUERY in query_params:
|
||||
inner_query_params = QueryDict(query_params.get(QS_QUERY), mutable=True)
|
||||
inner_query_params.pop(QS_KEY_TOKEN, None)
|
||||
query_params[QS_QUERY] = inner_query_params.urlencode()
|
||||
|
||||
query_params.update(kwargs)
|
||||
print(query_params)
|
||||
full_url = base_url
|
||||
if len(query_params) > 0:
|
||||
full_url = f"{full_url}?{query_params.urlencode()}"
|
||||
@ -75,7 +72,7 @@ class EmailStageView(ChallengeStageView):
|
||||
valid_delta = timedelta(
|
||||
minutes=current_stage.token_expiry + 1
|
||||
) # + 1 because django timesince always rounds down
|
||||
identifier = slugify(f"ak-email-stage-{current_stage.name}-{pending_user}")
|
||||
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
|
||||
# Don't check for validity here, we only care if the token exists
|
||||
tokens = FlowToken.objects.filter(identifier=identifier)
|
||||
if not tokens.exists():
|
||||
|
@ -13,6 +13,7 @@ from authentik.events.models import Event, EventAction
|
||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.utils import logo_data
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -81,6 +82,10 @@ def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Opti
|
||||
# Because we use the Message-ID as UID for the task, manually assign it
|
||||
message_object.extra_headers["Message-ID"] = message_id
|
||||
|
||||
# Add the logo (we can't add it in the previous message since MIMEImage
|
||||
# can't be converted to json)
|
||||
message_object.attach(logo_data())
|
||||
|
||||
LOGGER.debug("Sending mail", to=message_object.to)
|
||||
backend.send_messages([message_object])
|
||||
Event.new(
|
||||
|
@ -259,7 +259,7 @@ class TestEmailStage(FlowTestCase):
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
url += "?foo=bar"
|
||||
url += "?query=" + urlencode({"foo": "bar"})
|
||||
request = self.factory.get(url)
|
||||
stage_view = EmailStageView(
|
||||
FlowExecutorView(
|
||||
@ -273,31 +273,3 @@ class TestEmailStage(FlowTestCase):
|
||||
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
|
||||
f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
|
||||
)
|
||||
|
||||
def test_url_existing_params_nested(self):
|
||||
"""Test to ensure that URL params are preserved in the URL being sent (including nested)"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
url += "?foo=bar&"
|
||||
url += "query=" + urlencode({"nested": "value"})
|
||||
request = self.factory.get(url)
|
||||
stage_view = EmailStageView(
|
||||
FlowExecutorView(
|
||||
request=request,
|
||||
flow=self.flow,
|
||||
),
|
||||
request=request,
|
||||
)
|
||||
token = generate_id()
|
||||
self.assertEqual(
|
||||
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
|
||||
(
|
||||
f"http://testserver/if/flow/{self.flow.slug}"
|
||||
f"/?foo=bar&query=nested%3Dvalue&flow_token={token}"
|
||||
),
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ from django.utils import translation
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def logo_data():
|
||||
def logo_data() -> MIMEImage:
|
||||
"""Get logo as MIME Image for emails"""
|
||||
path = Path("web/icons/icon_left_brand.png")
|
||||
if not path.exists():
|
||||
@ -29,5 +29,4 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
super().__init__(**kwargs)
|
||||
self.content_subtype = "html"
|
||||
self.mixed_subtype = "related"
|
||||
self.attach(logo_data())
|
||||
self.attach_alternative(html_content, "text/html")
|
||||
|
@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
|
||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
@ -58,11 +59,33 @@ class TestUserWriteStage(FlowTestCase):
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
|
||||
self.assertTrue(user_qs.exists())
|
||||
self.assertTrue(user_qs.first().check_password(password))
|
||||
self.assertEqual(
|
||||
list(user_qs.first().ak_groups.order_by("name")), [self.other_group, self.group]
|
||||
user = user_qs.first()
|
||||
self.assertTrue(user.check_password(password))
|
||||
self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
|
||||
self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
|
||||
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED,
|
||||
context__model={
|
||||
"app": "authentik_core",
|
||||
"model_name": "user",
|
||||
"pk": user.pk,
|
||||
"name": "name",
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_UPDATED,
|
||||
context__model={
|
||||
"app": "authentik_core",
|
||||
"model_name": "user",
|
||||
"pk": user.pk,
|
||||
"name": "name",
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
|
||||
|
||||
def test_user_update(self):
|
||||
"""Test update of existing user"""
|
||||
|
@ -85,6 +85,19 @@ entries:
|
||||
identifiers:
|
||||
name: default-oobe-password-usable
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
- attrs:
|
||||
expression: |
|
||||
# This policy ensures that the setup flow can only be
|
||||
# used one time
|
||||
from authentik.flows.models import Flow, FlowAuthenticationRequirement
|
||||
Flow.objects.filter(slug="initial-setup").update(
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
|
||||
)
|
||||
return True
|
||||
id: policy-default-oobe-flow-set-authentication
|
||||
identifiers:
|
||||
name: default-oobe-flow-set-authentication
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
- attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-header
|
||||
@ -129,6 +142,7 @@ entries:
|
||||
evaluate_on_plan: true
|
||||
invalid_response_action: retry
|
||||
re_evaluate_policies: false
|
||||
id: binding-login
|
||||
identifiers:
|
||||
order: 100
|
||||
stage: !KeyOf stage-default-authentication-login
|
||||
@ -144,3 +158,8 @@ entries:
|
||||
policy: !KeyOf policy-default-oobe-prefill-user
|
||||
target: !KeyOf binding-password-write
|
||||
model: authentik_policies.policybinding
|
||||
- identifiers:
|
||||
order: 0
|
||||
policy: !KeyOf policy-default-oobe-flow-set-authentication
|
||||
target: !KeyOf binding-login
|
||||
model: authentik_policies.policybinding
|
||||
|
@ -42,9 +42,3 @@ entries:
|
||||
user: !KeyOf admin-user
|
||||
attrs:
|
||||
key: !Context token
|
||||
- model: authentik_blueprints.blueprintinstance
|
||||
identifiers:
|
||||
metadata:
|
||||
labels:
|
||||
blueprints.goauthentik.io/system-bootstrap: "true"
|
||||
state: absent
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -53,7 +53,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
4
go.mod
4
go.mod
@ -12,7 +12,7 @@ require (
|
||||
github.com/go-openapi/runtime v0.26.0
|
||||
github.com/go-openapi/strfmt v0.21.7
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
@ -27,7 +27,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
goauthentik.io/api/v3 v3.2023083.10
|
||||
goauthentik.io/api/v3 v3.2023101.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.13.0
|
||||
golang.org/x/sync v0.4.0
|
||||
|
7
go.sum
7
go.sum
@ -211,8 +211,9 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
@ -355,8 +356,8 @@ go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyK
|
||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
goauthentik.io/api/v3 v3.2023083.10 h1:mMCOfsqjouSSxedSkCK4k0Cwtt68CWzQgR7Um6ooOQs=
|
||||
goauthentik.io/api/v3 v3.2023083.10/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2023101.1 h1:KIQ4wmxjE+geAVB0wBfmxW9Uzo/tA0dbd2hSUJ7YJ3M=
|
||||
goauthentik.io/api/v3 v3.2023101.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.10.1"
|
||||
const VERSION = "2023.10.4"
|
||||
|
@ -50,7 +50,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
Domain: *p.CookieDomain,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: maxAge,
|
||||
Path: externalHost.Path,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
a.log.Trace("using redis session backend")
|
||||
@ -71,7 +71,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
cs.Options.Domain = *p.CookieDomain
|
||||
cs.Options.SameSite = http.SameSiteLaxMode
|
||||
cs.Options.MaxAge = maxAge
|
||||
cs.Options.Path = externalHost.Path
|
||||
cs.Options.Path = "/"
|
||||
a.log.WithField("dir", dir).Trace("using filesystem session backend")
|
||||
return cs
|
||||
}
|
||||
@ -131,7 +131,6 @@ func (a *Application) Logout(ctx context.Context, filter func(c Claims) bool) er
|
||||
}
|
||||
if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
|
||||
client := rs.Client()
|
||||
defer client.Close()
|
||||
keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1,5 +1,12 @@
|
||||
# Stage 1: Build
|
||||
FROM docker.io/golang:1.21.3-bookworm AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -13,7 +20,7 @@ ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
FROM gcr.io/distroless/static-debian11:debug
|
||||
|
@ -3,13 +3,17 @@ from lifecycle.migrate import BaseMigration
|
||||
|
||||
SQL_STATEMENT = """
|
||||
BEGIN TRANSACTION;
|
||||
DELETE FROM django_migrations WHERE app = 'otp_static';
|
||||
DELETE FROM django_migrations WHERE app = 'otp_totp';
|
||||
-- Update migrations (static)
|
||||
UPDATE django_migrations SET app = 'authentik_stages_authenticator_static', name = '0008_initial' WHERE app = 'otp_static' AND name = '0001_initial';
|
||||
UPDATE django_migrations SET app = 'authentik_stages_authenticator_static', name = '0009_throttling' WHERE app = 'otp_static' AND name = '0002_throttling';
|
||||
-- Rename tables (static)
|
||||
ALTER TABLE otp_static_staticdevice RENAME TO authentik_stages_authenticator_static_staticdevice;
|
||||
ALTER TABLE otp_static_statictoken RENAME TO authentik_stages_authenticator_static_statictoken;
|
||||
ALTER SEQUENCE otp_static_statictoken_id_seq RENAME TO authentik_stages_authenticator_static_statictoken_id_seq;
|
||||
ALTER SEQUENCE otp_static_staticdevice_id_seq RENAME TO authentik_stages_authenticator_static_staticdevice_id_seq;
|
||||
-- Update migrations (totp)
|
||||
UPDATE django_migrations SET app = 'authentik_stages_authenticator_totp', name = '0008_initial' WHERE app = 'otp_totp' AND name = '0001_initial';
|
||||
UPDATE django_migrations SET app = 'authentik_stages_authenticator_totp', name = '0009_auto_20190420_0723' WHERE app = 'otp_totp' AND name = '0002_auto_20190420_0723';
|
||||
-- Rename tables (totp)
|
||||
ALTER TABLE otp_totp_totpdevice RENAME TO authentik_stages_authenticator_totp_totpdevice;
|
||||
ALTER SEQUENCE otp_totp_totpdevice_id_seq RENAME TO authentik_stages_authenticator_totp_totpdevice_id_seq;
|
||||
@ -25,22 +29,9 @@ class Migration(BaseMigration):
|
||||
return bool(self.cur.rowcount)
|
||||
|
||||
def run(self):
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
self.fake_migration(
|
||||
(
|
||||
"authentik_stages_authenticator_static",
|
||||
"0008_initial",
|
||||
),
|
||||
(
|
||||
"authentik_stages_authenticator_static",
|
||||
"0009_throttling",
|
||||
),
|
||||
(
|
||||
"authentik_stages_authenticator_totp",
|
||||
"0008_initial",
|
||||
),
|
||||
(
|
||||
"authentik_stages_authenticator_totp",
|
||||
"0009_auto_20190420_0723",
|
||||
),
|
||||
self.cur.execute(
|
||||
"SELECT * FROM django_migrations WHERE app = 'authentik_stages_authenticator_static' AND name = '0007_authenticatorstaticstage_token_length_and_more';"
|
||||
)
|
||||
if not bool(self.cur.rowcount):
|
||||
raise Exception("Please upgrade to 2023.8 before upgrading to 2023.10")
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
|
73
poetry.lock
generated
73
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@ -2096,16 +2096,6 @@ files = [
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
@ -2780,13 +2770,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-scim"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description = "Pydantic types for SCIM"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "pydantic-scim-0.0.7.tar.gz", hash = "sha256:bc043da51c346051dfd372f12d1837c0846b815236340156d663a8514cba5761"},
|
||||
{file = "pydantic_scim-0.0.7-py3-none-any.whl", hash = "sha256:058eb195f75ef32d04eaf6369c125d5fb7052891694686f8e55e04d184ab1360"},
|
||||
{file = "pydantic-scim-0.0.8.tar.gz", hash = "sha256:b6c62031126e8c54f0fc7df837678e63934a5b068533fc52e5dfb6cfc24d59e9"},
|
||||
{file = "pydantic_scim-0.0.8-py3-none-any.whl", hash = "sha256:407b3bf55240947155c77a6dd839881d63368c61d64076d6b167ef124ceac79a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2840,10 +2830,7 @@ files = [
|
||||
[package.dependencies]
|
||||
astroid = ">=3.0.1,<=3.1.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||
]
|
||||
dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""}
|
||||
isort = ">=4.2.5,<6"
|
||||
mccabe = ">=0.6,<0.8"
|
||||
platformdirs = ">=2.2.0"
|
||||
@ -3096,7 +3083,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
@ -3104,15 +3090,8 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
@ -3129,7 +3108,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
@ -3137,7 +3115,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
@ -3374,28 +3351,28 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:0d3ee66b825b713611f89aa35d16de984f76f26c50982a25d52cd0910dff3923"},
|
||||
{file = "ruff-0.1.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f85f850a320ff532b8f93e8d1da6a36ef03698c446357c8c43b46ef90bb321eb"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:809c6d4e45683696d19ca79e4c6bd3b2e9204fe9546923f2eb3b126ec314b0dc"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46005e4abb268e93cad065244e17e2ea16b6fcb55a5c473f34fbc1fd01ae34cb"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10cdb302f519664d5e2cf954562ac86c9d20ca05855e5b5c2f9d542228f45da4"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f89ebcbe57a1eab7d7b4ceb57ddf0af9ed13eae24e443a7c1dc078000bd8cc6b"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7344eaca057d4c32373c9c3a7afb7274f56040c225b6193dd495fcf69453b436"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dffa25f6e03c4950b6ac6f216bc0f98a4be9719cb0c5260c8e88d1bac36f1683"},
|
||||
{file = "ruff-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ddaea52cb7ba7c785e8593a7532866c193bc774fe570f0e4b1ccedd95b83c5"},
|
||||
{file = "ruff-0.1.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8533efda625bbec0bf27da2886bd641dae0c209104f6c39abc4be5b7b22de2a"},
|
||||
{file = "ruff-0.1.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b0b1b82221ba7c50e03b7a86b983157b5d3f4d8d4f16728132bdf02c6d651f77"},
|
||||
{file = "ruff-0.1.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c1362eb9288f8cc95535294cb03bd4665c8cef86ec32745476a4e5c6817034c"},
|
||||
{file = "ruff-0.1.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ffa7ef5ded0563329a35bd5a1cfdae40f05a75c0cc2dd30f00b1320b1fb461fc"},
|
||||
{file = "ruff-0.1.2-py3-none-win32.whl", hash = "sha256:6e8073f85e47072256e2e1909f1ae515cf61ff5a4d24730a63b8b4ac24b6704a"},
|
||||
{file = "ruff-0.1.2-py3-none-win_amd64.whl", hash = "sha256:b836ddff662a45385948ee0878b0a04c3a260949905ad861a37b931d6ee1c210"},
|
||||
{file = "ruff-0.1.2-py3-none-win_arm64.whl", hash = "sha256:b0c42d00db5639dbd5f7f9923c63648682dd197bf5de1151b595160c96172691"},
|
||||
{file = "ruff-0.1.2.tar.gz", hash = "sha256:afd4785ae060ce6edcd52436d0c197628a918d6d09e3107a892a1bad6a4c6608"},
|
||||
{file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"},
|
||||
{file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"},
|
||||
{file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"},
|
||||
{file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"},
|
||||
{file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"},
|
||||
{file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4331,5 +4308,5 @@ files = [
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "e6b1df989cb5c50609540c1229d05d8458ef1cc343fb5868402db8b7679ad73c"
|
||||
python-versions = "~3.11"
|
||||
content-hash = "5a57dede617d149e0f307fc42580dcfd0d4b76161009dc447d6f10b048426c98"
|
||||
|
@ -15,7 +15,14 @@ COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM docker.io/golang:1.21.3-bookworm AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -29,7 +36,7 @@ ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
|
||||
|
||||
# Stage 3: Run
|
||||
FROM gcr.io/distroless/static-debian11:debug
|
||||
|
@ -113,7 +113,7 @@ filterwarnings = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.10.1"
|
||||
version = "2023.10.4"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
@ -151,10 +151,10 @@ packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
pycryptodome = "*"
|
||||
pydantic = "<3.0.0"
|
||||
pydantic-scim = "^0.0.7"
|
||||
pydantic = "*"
|
||||
pydantic-scim = "*"
|
||||
pyjwt = "*"
|
||||
python = "^3.11"
|
||||
python = "~3.11"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = "*"
|
||||
|
@ -1,5 +1,12 @@
|
||||
# Stage 1: Build
|
||||
FROM docker.io/golang:1.21.3-bookworm AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -13,7 +20,7 @@ ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -o /go/radius ./cmd/radius
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
|
||||
|
||||
# Stage 2: Run
|
||||
FROM gcr.io/distroless/static-debian11:debug
|
||||
|
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.10.1
|
||||
version: 2023.10.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
@ -27,5 +27,8 @@
|
||||
"precommit": "run-s lint:precommit lint:spelling prettier",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
11
web/package-lock.json
generated
11
web/package-lock.json
generated
@ -17,7 +17,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@goauthentik/api": "^2023.10.0-1698336292",
|
||||
"@goauthentik/api": "^2023.10.1-1698348102",
|
||||
"@lit-labs/context": "^0.4.1",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/localize": "^0.11.4",
|
||||
@ -100,6 +100,9 @@
|
||||
"typescript": "^5.2.2",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.19.5",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
@ -2883,9 +2886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2023.10.0-1698336292",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.10.0-1698336292.tgz",
|
||||
"integrity": "sha512-CZd9d6b0pFR/rbD91+enULWN3JhivHVgjDIHE927MEv2wpqtl5koby7VaMNNM9WL8LQdgzCdJt4nstjnhm21tw=="
|
||||
"version": "2023.10.1-1698348102",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.10.1-1698348102.tgz",
|
||||
"integrity": "sha512-Melx4hoHOLbgAOHREGzx83uN5BKvgql4qIUloxh/abvNeGLlfKL49caiU8++ANUaERr1vb8X2tHFwiwxtqXKeQ=="
|
||||
},
|
||||
"node_modules/@hcaptcha/types": {
|
||||
"version": "1.0.3",
|
||||
|
@ -38,7 +38,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@goauthentik/api": "^2023.10.0-1698336292",
|
||||
"@goauthentik/api": "^2023.10.1-1698348102",
|
||||
"@lit-labs/context": "^0.4.1",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/localize": "^0.11.4",
|
||||
@ -125,5 +125,8 @@
|
||||
"@esbuild/darwin-arm64": "^0.19.5",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.19.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
|
||||
return app;
|
||||
}
|
||||
|
||||
handleConfirmBackchannelProviders({ items }: { items: Provider[] }) {
|
||||
handleConfirmBackchannelProviders(items: Provider[]) {
|
||||
this.backchannelProviders = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
|
@ -63,7 +63,7 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${this.label} name=${name}>
|
||||
<div class="pf-c-input-group">
|
||||
<ak-provider-select-table ?backchannelOnly=${true} .confirm=${confirm}>
|
||||
<ak-provider-select-table ?backchannelOnly=${true} .confirm=${this.confirm}>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
${this.tooltip ? this.tooltip : nothing}
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
|
@ -114,8 +114,8 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
||||
this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
|
@ -78,8 +78,8 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Mode")} ?required=${true} name="mode">
|
||||
<ak-radio
|
||||
@change=${(ev: CustomEvent<NotificationTransportModeEnum>) => {
|
||||
this.onModeChange(ev.detail);
|
||||
@change=${(ev: CustomEvent<{ value: NotificationTransportModeEnum }>) => {
|
||||
this.onModeChange(ev.detail.value);
|
||||
}}
|
||||
.options=${[
|
||||
{
|
||||
|
@ -210,8 +210,8 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<ClientTypeEnum>) => {
|
||||
this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
@ -334,13 +334,14 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-switch-input name="includeClaimsInIdToken">
|
||||
<ak-switch-input
|
||||
name="includeClaimsInIdToken"
|
||||
label=${msg("Include claims in id_token")}
|
||||
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||
help=${msg(
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}></ak-switch-input
|
||||
>
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
label=${msg("Issuer mode")}
|
||||
|
@ -42,15 +42,13 @@ export class RoleForm extends ModelForm<Role, string> {
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
return html`<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
@ -386,6 +386,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Consumer secret")}
|
||||
@ -394,6 +395,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
name="consumerSecret"
|
||||
>
|
||||
<textarea class="pf-c-form-control"></textarea>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client Secret.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
|
@ -15,6 +15,8 @@ export class UserDeviceTable extends Table<Device> {
|
||||
@property({ type: Number })
|
||||
userId?: number;
|
||||
|
||||
checkbox = true;
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Device>> {
|
||||
return new AuthenticatorsApi(DEFAULT_CONFIG)
|
||||
.authenticatorsAdminAllList({
|
||||
@ -64,6 +66,21 @@ export class UserDeviceTable extends Table<Device> {
|
||||
}
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Device(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Device) => {
|
||||
return this.deleteWrapper(item);
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html` <ak-spinner-button
|
||||
.callAction=${() => {
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.10.1";
|
||||
export const VERSION = "2023.10.4";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -80,11 +80,12 @@ export class IdentificationStage extends BaseStage<
|
||||
}
|
||||
|
||||
createHelperForm(): void {
|
||||
const compatMode = "ShadyDOM" in window;
|
||||
this.form = document.createElement("form");
|
||||
document.documentElement.appendChild(this.form);
|
||||
// Only add the additional username input if we're in a shadow dom
|
||||
// otherwise it just confuses browsers
|
||||
if (!("ShadyDOM" in window)) {
|
||||
if (!compatMode) {
|
||||
// This is a workaround for the fact that we're in a shadow dom
|
||||
// adapted from https://github.com/home-assistant/frontend/issues/3133
|
||||
const username = document.createElement("input");
|
||||
@ -104,30 +105,33 @@ export class IdentificationStage extends BaseStage<
|
||||
};
|
||||
this.form.appendChild(username);
|
||||
}
|
||||
const password = document.createElement("input");
|
||||
password.setAttribute("type", "password");
|
||||
password.setAttribute("name", "password");
|
||||
password.setAttribute("autocomplete", "current-password");
|
||||
password.onkeyup = (ev: KeyboardEvent) => {
|
||||
if (ev.key == "Enter") {
|
||||
this.submitForm(ev);
|
||||
}
|
||||
const el = ev.target as HTMLInputElement;
|
||||
// Because the password field is not actually on this page,
|
||||
// and we want to 'prefill' the password for the user,
|
||||
// save it globally
|
||||
PasswordManagerPrefill.password = el.value;
|
||||
// Because password managers fill username, then password,
|
||||
// we need to re-focus the uid_field here too
|
||||
(this.shadowRoot || this)
|
||||
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||
.forEach((input) => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
this.form.appendChild(password);
|
||||
// Only add the password field when we don't already show a password field
|
||||
if (!compatMode && !this.challenge.passwordFields) {
|
||||
const password = document.createElement("input");
|
||||
password.setAttribute("type", "password");
|
||||
password.setAttribute("name", "password");
|
||||
password.setAttribute("autocomplete", "current-password");
|
||||
password.onkeyup = (ev: KeyboardEvent) => {
|
||||
if (ev.key == "Enter") {
|
||||
this.submitForm(ev);
|
||||
}
|
||||
const el = ev.target as HTMLInputElement;
|
||||
// Because the password field is not actually on this page,
|
||||
// and we want to 'prefill' the password for the user,
|
||||
// save it globally
|
||||
PasswordManagerPrefill.password = el.value;
|
||||
// Because password managers fill username, then password,
|
||||
// we need to re-focus the uid_field here too
|
||||
(this.shadowRoot || this)
|
||||
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||
.forEach((input) => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
this.form.appendChild(password);
|
||||
}
|
||||
const totp = document.createElement("input");
|
||||
totp.setAttribute("type", "text");
|
||||
totp.setAttribute("name", "code");
|
||||
|
@ -5798,12 +5798,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -6031,6 +6025,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6079,12 +6079,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -6312,6 +6306,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5713,12 +5713,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -5946,6 +5940,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
2989
web/xliff/fr.xlf
2989
web/xliff/fr.xlf
File diff suppressed because it is too large
Load Diff
@ -5921,12 +5921,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -6154,6 +6148,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7557,14 +7557,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
<target>Ƒōŕ ţŕàńśƥàŕēńţ ŕēvēŕśē ƥŕōxĩēś ŵĩţĥ ŕēǫũĩŕēď àũţĥēńţĩćàţĩōń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ ƥēŕ ŕōōţ ďōmàĩń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
<target>Ćōńƒĩĝũŕē ŚÀMĹ ƥŕōvĩďēŕ màńũàĺĺŷ</target>
|
||||
@ -7844,4 +7836,16 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
@ -5706,12 +5706,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -5939,6 +5933,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="s4caed5b7a7e5d89b">
|
||||
@ -613,9 +613,9 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="saa0e2675da69651b">
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s58cd9c2fe836d9c6">
|
||||
@ -1057,8 +1057,8 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa8384c9c26731f83">
|
||||
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s55787f4dfcdce52b">
|
||||
@ -1799,8 +1799,8 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa90b7809586c35ce">
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s0410779cb47de312">
|
||||
@ -2562,31 +2562,6 @@
|
||||
<source>If the password's score is less than or equal this value, the policy will fail.</source>
|
||||
<target>如果密码分数小于等于此值,则策略失败。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1bfe7505059d164f">
|
||||
<source>0: Too guessable: risky password. (guesses < 10^3)</source>
|
||||
<target>0:过于易猜测:密码有风险。(猜测次数 < 10^3)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s423d1f2477998d0b">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
|
||||
<target>1:非常易猜测:可以防范受限的在线攻击。(猜测次数 < 10^6)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s33849cc046eb901d">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
|
||||
<target>2:有些易猜测:可以防范不受限的在线攻击。(猜测次数 < 10^8)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s578dcce295718e1b">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
|
||||
<target>3:难以猜测:适度防范离线慢速哈希场景。(猜测次数 < 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7a46de49f4eba5d7">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
|
||||
<target>4:非常难以猜测:高度防范离线慢速哈希场景。(猜测次数 >= 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sd6cd7ce2310a73a4">
|
||||
<source>Checks the value from the policy request against several rules, mostly used to ensure password strength.</source>
|
||||
@ -3013,8 +2988,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s76768bebabb7d543">
|
||||
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
||||
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
||||
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
||||
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s026555347e589f0e">
|
||||
@ -3806,8 +3781,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7b1fba26d245cb1c">
|
||||
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
||||
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
||||
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
||||
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s44536d20bb5c8257">
|
||||
@ -3816,8 +3791,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s3bb51cabb02b997e">
|
||||
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
|
||||
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
|
||||
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
|
||||
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s04bfd02201db5ab8">
|
||||
@ -4013,10 +3988,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa95a538bfbb86111">
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<target>您确定要更新
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92d7cfb6ee1fec6">
|
||||
@ -5102,7 +5077,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sdf1d8edef27236f0">
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<target>像 YubiKey 这样的“漫游”身份验证器</target>
|
||||
|
||||
</trans-unit>
|
||||
@ -5437,10 +5412,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s2d5f69929bb7221d">
|
||||
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
|
||||
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
|
||||
<target>
|
||||
<x id="0" equiv-text="${prompt.name}"/>("
|
||||
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
|
||||
<x id="0" equiv-text="${prompt.name}"/>("
|
||||
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
|
||||
<x id="2" equiv-text="${prompt.type}"/>)</target>
|
||||
|
||||
</trans-unit>
|
||||
@ -5489,7 +5464,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
|
||||
|
||||
</trans-unit>
|
||||
@ -7620,14 +7595,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
<target>适用于需要验证身份的透明反向代理</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>适用于 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>适用于按根域名配置的 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
<target>手动配置 SAML 提供程序</target>
|
||||
@ -7933,7 +7900,95 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
<target><未设置名称></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>适用于 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>适用于按根域名配置的 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
<target>RBAC 目前处于预览状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
<target>新创建用户使用的用户类型。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4a34a6be4c68ec87">
|
||||
<source>Users created</source>
|
||||
<target>已创建用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s275c956687e2e656">
|
||||
<source>Failed logins</source>
|
||||
<target>失败登录</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
<target>也称为客户端 ID。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
<target>也称为客户端密钥。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4476e9c50cfd13f4">
|
||||
<source>Global status</source>
|
||||
<target>全局状态</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
<target>供应商</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
<target>无同步状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
<target>当前正在同步。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf36170f71cea38c2">
|
||||
<source>Connectivity</source>
|
||||
<target>连接性</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd94e99af8b41ff54">
|
||||
<source>0: Too guessable: risky password. (guesses &lt; 10^3)</source>
|
||||
<target>0:过于易猜测:密码有风险。(猜测次数 &lt; 10^3)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc926385d1a624c3a">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses &lt; 10^6)</source>
|
||||
<target>1:非常易猜测:可以防范受限的在线攻击。(猜测次数 &lt; 10^6)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8aae61c41319602c">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses &lt; 10^8)</source>
|
||||
<target>2:有些易猜测:可以防范不受限的在线攻击。(猜测次数 &lt; 10^8)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1f4b57e722a89d6">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses &lt; 10^10)</source>
|
||||
<target>3:难以猜测:适度防范离线慢速哈希场景。(猜测次数 &lt; 10^10)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd47f3d3c9741343d">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses &gt;= 10^10)</source>
|
||||
<target>4:非常难以猜测:高度防范离线慢速哈希场景。(猜测次数 &gt;= 10^10)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d2a8b86a4f5a810">
|
||||
<source>Successfully created user and added to group <x id="0" equiv-text="${this.group.name}"/></source>
|
||||
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s824e0943a7104668">
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||
<source>Pretend user exists</source>
|
||||
<target>假作用户存在</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52bdc80690a9a8dc">
|
||||
<source>When enabled, the stage will always accept the given user identifier and continue.</source>
|
||||
<target>启用时,此阶段总是会接受指定的用户 ID 并继续。</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -5754,12 +5754,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -5987,6 +5981,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7620,14 +7620,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
<target>适用于需要验证身份的透明反向代理</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>适用于 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>适用于按根域名配置的 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
<target>手动配置 SAML 提供程序</target>
|
||||
@ -7933,6 +7925,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
<target><未设置名称></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>适用于 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>适用于按根域名配置的 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
<target>RBAC 目前处于预览状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
<target>新创建用户使用的用户类型。</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5753,12 +5753,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s945a6b94361ee45b">
|
||||
<source>For transparent reverse proxies with required authentication</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadf073913458acbd">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se770e9498b3bacf6">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s40830ec037f34626">
|
||||
<source>Configure SAML provider manually</source>
|
||||
</trans-unit>
|
||||
@ -5986,6 +5980,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0924f51b028233a3">
|
||||
<source><No name set></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -0,0 +1,130 @@
|
||||
---
|
||||
title: 3 ways you (might be) doing containers wrong
|
||||
description: “Using containers is not a best practice in itself. Here are some mistakes beginners make with containers, and how we set them up correctly at authentik.”
|
||||
authors:
|
||||
- name: Jens Langhammer
|
||||
title: CTO at Authentik Security Inc
|
||||
url: https://github.com/BeryJu
|
||||
image_url: https://github.com/BeryJu.png
|
||||
tags:
|
||||
- application
|
||||
- runtime
|
||||
- SSO
|
||||
- Docker
|
||||
- containers
|
||||
- :latest
|
||||
- identity provider
|
||||
- security
|
||||
- authentication
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
_authentik is an open source Identity Provider that unifies your identity needs into a single platform, replacing Okta, Active Directory, and Auth0. Authentik Security is a [public benefit company](https://github.com/OpenCoreVentures/ocv-public-benefit-company/blob/main/ocv-public-benefit-company-charter.md) building on top of the open source project._
|
||||
|
||||
---
|
||||
|
||||
There are two ways to judge an application:
|
||||
|
||||
1. Does it do what it’s supposed to do?
|
||||
2. Is it easy to run?
|
||||
|
||||
This post is about the second.
|
||||
|
||||
Using containers is not a best practice in itself. As an infrastructure engineer by background, I’m pretty opinionated about how to set up containers properly. Doing things the “right” way makes things easier not just for you, but for your users as well.
|
||||
|
||||
Below are some common mistakes that I see beginners make with containers:
|
||||
|
||||
1. Using one container per application
|
||||
2. Installing things at runtime
|
||||
3. Writing logs to files instead of stdout
|
||||
|
||||
## Mistake #1: One container per application
|
||||
|
||||
There tend to be two mindsets when approaching setting up containers:
|
||||
|
||||
- The inexperienced usually think 1 container = 1 application
|
||||
- The other option is 1 container = 1 service
|
||||
|
||||
Your application usually consists of multiple services, and to my mind these should always be separated into their own containers (in keeping with the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)).
|
||||
|
||||
For example, authentik consists of four components (services):
|
||||
|
||||
- Server
|
||||
- Worker
|
||||
- Database
|
||||
- Cache
|
||||
|
||||
With our deployment, that means you get four different containers because they each run one of those four services.
|
||||
|
||||
### Why you should use one container per _service_
|
||||
|
||||
At the point where you need to scale, or need High Availability, having different processes in separate containers enables horizontal scaling. Because of how authentik deploys, if we need to handle more traffic we can scale up to 50 servers, rather than having to scale up _everything_. This wouldn’t work if all those components were all bundled together.
|
||||
|
||||
Additionally, if you’re using a container orchestrator (whether that’s Kubernetes or something simpler like [Docker Compose](https://goauthentik.io/docs/installation/docker-compose)), if it’s all bundled together, the orchestrator can’t distinguish between components because they’re all in the black box of your container.
|
||||
|
||||
Say you want to start up processes in a specific order. This isn’t possible if they’re in a single container (unless you rebuild the entire image). If those processes are separate, you can just tell Docker Compose to start them up in the order you want, or you can run specific components on specific servers.
|
||||
|
||||
Of course, your application architecture and deployment model need to support this setup, which is why it’s critical to think about these things when you’re starting out. If you’re reading this and thinking, I have a small-scale, hobby project, this doesn’t apply to me—let me put it this way: you will never regret setting things up the “right” way. It’s not going to come back to bite you if your situation changes later. It also gives users who install the application a lot more freedom and flexibility in how _they_ want to run it.
|
||||
|
||||
## Mistake #2: Installing things at runtime
|
||||
|
||||
Your container image should be complete in itself: it should contain all code and dependencies—everything it needs to run. This is the point of a container—it’s self contained.
|
||||
|
||||
I’ve seen people set up their container to download an application from the vendor and install it into the container on startup. While this does work, what happens if you don’t have internet access? What if the vendor shut down and that URL now points to a malicious bit of code?
|
||||
|
||||
If you have 100 instances downloading files at startup (or end up scaling to that point), this can lead to rate limiting, failed downloads, or your internet connection getting saturated—it’s just inefficient and causes problems that can be avoided.
|
||||
|
||||
### Also, don’t use :latest
|
||||
|
||||
This leads me to a different but related bad practice: using the `:latest` tag. It’s a common pitfall for folks who use containers but don’t necessarily build them themselves.
|
||||
|
||||
It’s easy to get started with the `:latest` tag and it’s understandable to want the latest version without having to go into files and manually edit everything. But what can happen is that you update and suddenly it’s pointing to a new version and breaking things.
|
||||
|
||||
I’ve seen this happen where you’re just running something on a local server and your disk is full, so you empty out your Docker images. The next time you pull, it’s with a new version which now no longer works and you’re stuck trying to figure out what version you were on before.
|
||||
|
||||
### Instead: Pin your dependencies
|
||||
|
||||
You should be pinning your dependencies to a specific version, and updating to newer versions intentionally rather than by default.
|
||||
|
||||
The most reliable way to do this is with a process called GitOps:
|
||||
|
||||
- In the context of Kubernetes, all the YAML files you deploy with Kubernetes are stored in the central Git repository.
|
||||
- You have software in your Kubernetes cluster that automatically pulls the files from your Git repo and installs them into the cluster.
|
||||
- Then you can use a tool like [Dependabot](https://github.com/dependabot) or [Renovate](https://github.com/renovatebot/renovate) to automatically create PRs with a new version (if there is one) so you can test and approve it, and it’s all captured in your Git history.
|
||||
|
||||
GitOps might be a bit excessive if you’re only running a small hobby project on a single server, but in any case you should still pin a version.
|
||||
|
||||
For a long time, authentik purposefully didn’t have a `:latest` tag, because people would use it inadvertently (sometimes not realizing they had an auto-updater running). Suddenly something wouldn’t work and there wasn’t really a way to downgrade.
|
||||
|
||||
We have since added it due to popular request. This is how authentik’s version tags work:
|
||||
|
||||
- Our version number is 3 digits reflecting the date of the release, so the latest currently is [2023.10.1](https://goauthentik.io/docs/releases/2023.10).
|
||||
- You can either use 2023.10.1 as the tag, pinning to that specific version
|
||||
- You can pin to 2023.10, which you means that you always get the latest patch version, or
|
||||
- You can use 2023, which means you always get the latest version within that year.
|
||||
|
||||
The principle is roughly the same with any project using [SemVer](https://semver.org/): you could just lock to v1, which means you get the latest v1 with all minor patches and fixes, without breaking updates. Then you switch to v2 when you’re ready.
|
||||
|
||||
With this approach you are putting some trust in the developer not to publish any breaking changes with the wrong version number (but you’re technically always putting trust in some developer when using someone else’s software!).
|
||||
|
||||
## Mistake #3: Writing logs to files instead of stdout
|
||||
|
||||
This is another issue on the infrastructure side that mainly happens when you put legacy applications into containers. It used to be standard that applications put their log output into a file, and you’d probably have a system daemon set up to rotate those files and archive the old ones. This was great when everything ran on the same server without containers.
|
||||
|
||||
A lot of software still logs to files by default, but this makes collecting and aggregating your services logs much harder. Docker (and containers in general) expect that you log to standard output so your orchestration platform can route the logs to your monitoring tool of choice.
|
||||
|
||||
Docker puts the logs into a JSON file that it can read itself and see the timestamps and which container the log refers to. You can set up log forwarding with both Docker and Kubernetes. If you have a central logging server, the plugin gets the standard output of a container and sends it to that server.
|
||||
|
||||
Not logging to `stdout` just makes it harder for everyone, including making it harder to debug: Instead of just running `docker logs` + the name of the container, you need to `exec` into the container, go to find the files, then look at the files to start debugging.
|
||||
|
||||
### This bad practice is arguably the easiest one to work around
|
||||
|
||||
As an engineer you can easily redirect the logs back from a file into the standard output, but there’s no real reason not to do it the “correct” way.
|
||||
|
||||
There aren’t many use cases where there’s an advantage to writing your logs directly to a file instead of stdout—in fact the main one is for when you’re making the first mistake (having your whole application in one container)! If you’re running multiple services in one container, then you’ll have logs from multiple different processes in one place, which _could_ be easier to work with in a file vs stdout.
|
||||
|
||||
Even if you specifically want your logs to exist in a file, by default if you run `docker logs` it just reads a JSON file that it adds the logs to, so you’re not losing anything by logging to stdout. You can configure Docker to just put the logs into a plain text file wherever you want to.
|
||||
|
||||
It’s a little simplistic, but I’d encourage you to check out [The Twelve-Factor App](https://12factor.net/) which outlines good practices for making software that’s easy to run.
|
||||
|
||||
Are you doing containers differently and is it working for you? Let us know in the comments, or send us an email at hello@goauthentik.io!
|
@ -3,7 +3,15 @@ title: Release 2023.10
|
||||
slug: "/releases/2023.10"
|
||||
---
|
||||
|
||||
<!-- ## Breaking changes -->
|
||||
## Breaking changes
|
||||
|
||||
- Requests with missing trailing slash are no longer redirected
|
||||
|
||||
In previous versions, requests to a path like `/api/v3/core/users` would be redirected to `[...]/users/`. This redirect would cause mutating requests (such as POST, PUT and PATCH) to fail as they would get redirected to `[...]/users/`. The redirect has been disabled, which will not have have an impact on a correctly configured setup.
|
||||
|
||||
- It is only possible to upgrade to 2023.10 from 2023.8. This is due to a bug in the migrations which will be fixed in a future release (#7326).
|
||||
|
||||
- Warning: The first 2024.x version of this chart will see a rework that will include breaking changes. The breaking changes will be noted in the next Release notes.
|
||||
|
||||
## New features
|
||||
|
||||
@ -13,7 +21,7 @@ slug: "/releases/2023.10"
|
||||
|
||||
- LDAP Provider improvements
|
||||
|
||||
The LDAP Provider now has an expanded schema, increasing the compatibility with clients that use the LDAP schema to parse data and .net applications on Windows.
|
||||
The LDAP Provider now has an expanded schema, increasing the compatibility with clients that use the LDAP schema to parse data and .Net applications on Windows.
|
||||
|
||||
- Improved Proxy provider logout
|
||||
|
||||
@ -113,6 +121,22 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2023.10
|
||||
- web: change 'Attributes' to 'Custom attributes' on Invitation Field (#7145)
|
||||
- web: the return of pseudolocalization (#7190)
|
||||
|
||||
## Fixed in 2023.10.1
|
||||
|
||||
- lifecycle: fix otp merge migration (#7315)
|
||||
|
||||
## Fixed in 2023.10.2
|
||||
|
||||
- \*: fix [GHSA-rjvp-29xq-f62w](../security/GHSA-rjvp-29xq-f62w), Reported by [@devSparkle](https://github.com/devSparkle)
|
||||
- blueprints: fix entries with state: absent not being deleted if their serializer has errors (#7345)
|
||||
- crypto: fix race conditions when creating self-signed certificates on startup (#7344)
|
||||
- lifecycle: rework otp_merge migration (#7359)
|
||||
- providers/proxy: to fix duplicate cookie (#7324)
|
||||
- rbac: handle lookup error (#7341)
|
||||
- stages/email: fix sending emails from task (#7325)
|
||||
- web/admin: fix @change handler for ak-radio elements (#7348)
|
||||
- web/admin: fix role form reacting to enter (#7330)
|
||||
|
||||
## API Changes
|
||||
|
||||
#### What's New
|
||||
|
61
website/docs/security/CVE-2023-48228.md
Normal file
61
website/docs/security/CVE-2023-48228.md
Normal file
@ -0,0 +1,61 @@
|
||||
# CVE-2023-48228
|
||||
|
||||
_Reported by [@Sapd](https://github.com/Sapd)_
|
||||
|
||||
## OAuth2: Insufficient PKCE check
|
||||
|
||||
### Summary
|
||||
|
||||
When initialising a OAuth2 flow with a `code_challenge` and `code_method` (thus requesting PKCE), the SSO provider (authentik) **must** check if there is a matching **and** existing `code_verifier` during the token step.
|
||||
|
||||
authentik checks if the contents of code*verifier is matching \*\*\_ONLY*\*\* when it is provided. When it is left out completely, authentik simply accepts the token request with out it; even when the flow was started with a `code_challenge`.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2023.8.5 and 2023.10.4 fix this issue.
|
||||
|
||||
### Details
|
||||
|
||||
The `code_verifier` is only checked when the user provides it. Note that in line 209 there is a check if the code_parameter is left out. But there is no check if the PKCE parameter simply was omitted WHEN the request was started with a `code_challenge_method`.
|
||||
|
||||
This oversight likely did not stem from a coding error but from a misinterpretation of the RFC, where the backward compatibility section may be somewhat confusing.
|
||||
https://datatracker.ietf.org/doc/html/rfc7636#section-4.5
|
||||
RFC7636 explicitly says in Section 4.5:
|
||||
|
||||
> The "code_challenge_method" is bound to the Authorization Code when
|
||||
> the Authorization Code is issued. That is the method that the token
|
||||
> endpoint MUST use to verify the "code_verifier".
|
||||
|
||||
Section 5, Compatibility
|
||||
|
||||
> Server implementations of this specification MAY accept OAuth2.0
|
||||
> clients that do not implement this extension. If the "code_verifier"
|
||||
> is not received from the client in the Authorization Request, servers
|
||||
> supporting backwards compatibility revert to the OAuth 2.0 [[RFC6749](https://datatracker.ietf.org/doc/html/rfc6749)]
|
||||
> protocol without this extension.
|
||||
|
||||
Section 5, Compatibility, allows server implementations of this specification to accept OAuth 2.0 clients that do not implement this extension. However, if a `code_verifier` is not received from the client in the Authorization Request, servers that support backward compatibility should revert to the standard OAuth 2.0 protocol sans this extension (including all steps).
|
||||
|
||||
It should be noted that this does not mean that the `code_verifier` check can be disregarded at any point if the initial request included `code_challenge` or `code_challenge_method`. Since Authentik supports PKCE, it **MUST** verify the code_verifier as described in Section 4.5 **AND** fail if it was not provided.
|
||||
|
||||
Ofc verification can be skipped if the original authorization request did not invoke PKCE (no `code_challenge_method` and no `code_challenge`).
|
||||
|
||||
Failure to check the `code_verifier` renders the PKCE flow ineffective. This vulnerability particularly endangers public or hybrid clients, as their `code` is deemed non-confidential.
|
||||
|
||||
While not explicitly stated in the standard, it is generally recommended that OAuth2 flows accepting public clients should enforce PKCE - at least when redirecting to a non HTTPS URL (like http or an app link).
|
||||
|
||||
### Impact
|
||||
|
||||
The vulnerability poses a high risk to both public and hybrid clients.
|
||||
When for example a mobile app implements oauth2, a malicious app can simply also register the same in-app-link (e.g. `mycoolapp://oauth2`) for the redirect callback URL, possibly receiving `code` during callback. With PKCE working, a malicious app would still receive a `code` but the `code` would not work without the correct unhashed code-challenge.
|
||||
This is especially problematic, because authentik claims to support PKCE, and a developer can expect that the proper checks are in place. Note that app-links cannot be protected by HTTPS or similar mechanisms.
|
||||
|
||||
Note also that this vulnerability poses a threat to confidential clients. Many confidential clients act as a proxy for OAuth2 API requests, typically from mobile apps or single-page applications. These proxies relay `code_challenge`, `code_challenge_method` (in auth request, which most libraries force and provide on default settings) and `code_verifier` in the token request unchanged and supplement the CLIENT_SECRET which only the relay knows. The relay can but does not have to check for an existing `code_verifier` as the standard does not define that PKCE can be ignored on confidential clients during the token request when the client requested PKCE during the authorization request.
|
||||
|
||||
An attacker could potentially gain full access to the application. If the code grants access to an admin account, the confidentiality, integrity, and availability of that application are compromised.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
27
website/docs/security/GHSA-rjvp-29xq-f62w.md
Normal file
27
website/docs/security/GHSA-rjvp-29xq-f62w.md
Normal file
@ -0,0 +1,27 @@
|
||||
# GHSA-rjvp-29xq-f62w
|
||||
|
||||
_Reported by [@devSparkle](https://github.com/devSparkle)_
|
||||
|
||||
## Potential Installation takeover when default admin user is deleted
|
||||
|
||||
### Summary
|
||||
|
||||
In the affected versions, when the default admin user has been deleted, it is potentially possible for an attacker to set the password of the default admin user without any authentication.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2023.8.4 and 2023.10.2 fix this issue, for other versions the workaround can be used.
|
||||
|
||||
### Impact
|
||||
|
||||
authentik uses a blueprint to create the default admin user, which can also optionally set the default admin users' password from an environment variable. When the user is deleted, the `initial-setup` flow used to configure authentik after the first installation becomes available again.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Ensure the default admin user (Username `akadmin`) exists and has a password set. It is recommended to use a very strong password for this user, and store it in a secure location like a password manager. It is also possible to deactivate the user to prevent any logins as akadmin.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
@ -46,5 +46,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
@ -407,6 +407,8 @@ const docsSidebar = {
|
||||
},
|
||||
items: [
|
||||
"security/policy",
|
||||
"security/CVE-2023-48228",
|
||||
"security/GHSA-rjvp-29xq-f62w",
|
||||
"security/CVE-2023-39522",
|
||||
"security/CVE-2023-36456",
|
||||
"security/2023-06-cure53",
|
||||
|
Reference in New Issue
Block a user