Compare commits
146 Commits
deny-unaut
...
permission
Author | SHA1 | Date | |
---|---|---|---|
ff787a0f59 | |||
ad27f268dc | |||
a3f86115e1 | |||
75eb025ef4 | |||
efb3803371 | |||
904d6cd81b | |||
b445cff4c9 | |||
89437ac73b | |||
e354e110ca | |||
cf5eea74ee | |||
54433e614a | |||
78a02ff1f0 | |||
749e015414 | |||
2c9bf4befe | |||
f14b2fd4c5 | |||
cda764c5fd | |||
4cee9f3a31 | |||
9972b43399 | |||
d4805f326f | |||
38864e8e9a | |||
5618545248 | |||
876feccd51 | |||
2e28683381 | |||
5d803a9bf3 | |||
c7b3272cf6 | |||
2688fa4fe8 | |||
b713660e5d | |||
de237aab10 | |||
4068d67424 | |||
ab6595b597 | |||
0f89b6b746 | |||
45f74debd9 | |||
5a52225ee2 | |||
d36f0d187b | |||
b7bfbff2fe | |||
46d8be8d20 | |||
58158f61e4 | |||
9543800442 | |||
c0adac3625 | |||
cd7dce2cae | |||
09570a30f9 | |||
8617bb098d | |||
c47fb2612a | |||
23c0d90b3e | |||
593ae3b52e | |||
7a62965928 | |||
2d060576c7 | |||
a51252e1d3 | |||
20904776bb | |||
4a50c1f640 | |||
41555c88c4 | |||
408e6ec34e | |||
5bc65e253b | |||
f5d1f72d22 | |||
ec9e815e7a | |||
b0671e26c8 | |||
f185a41813 | |||
a2211135bc | |||
b082849fb5 | |||
e933fd5692 | |||
38649e5347 | |||
ff91ecf873 | |||
15ee17ea60 | |||
75a6d8c0c5 | |||
ef4d532b9c | |||
985d491073 | |||
2bdc415068 | |||
547e5be7a2 | |||
1bc99e48e0 | |||
349f66e53c | |||
9e0a9f4eee | |||
727404c9a4 | |||
0fa4637640 | |||
afdf830e8a | |||
7ab636e103 | |||
4efb4d6191 | |||
b855d98b78 | |||
354634cdf4 | |||
319f2ef8d1 | |||
cf58c5617a | |||
71344d0b6a | |||
696db2ae05 | |||
f08da8f295 | |||
89106c8131 | |||
f6b0eecde7 | |||
4ca151ee14 | |||
f66fea4b0a | |||
6d8dc4ac43 | |||
04982c8147 | |||
2ab68480a0 | |||
248d9e48bb | |||
e58e4bdbae | |||
a07ce35985 | |||
cfe275a374 | |||
7f474cde19 | |||
0597a3450b | |||
8191b90126 | |||
2613a5da4b | |||
2c4dd232a1 | |||
6b5c11ccfd | |||
a0b3d37b4a | |||
56eca6dc8f | |||
0377da2779 | |||
b16c67cc82 | |||
28f55635be | |||
8d4b2610b1 | |||
419cf80469 | |||
632dc4b1b2 | |||
93cfa64f5a | |||
fa8f9d4017 | |||
d4c0696a8c | |||
20635a8cc6 | |||
c621ac0a6f | |||
0487c8d0f5 | |||
37511f07a0 | |||
7840a3b52a | |||
787e9e05e4 | |||
3c14b8931f | |||
e3f1d259cf | |||
3d981f9391 | |||
ba1c919781 | |||
38696d4bd9 | |||
7213a1f27a | |||
34b5a51990 | |||
79e779b339 | |||
2a35b13ad6 | |||
3754f27275 | |||
b0547844b9 | |||
1b5abd3a3a | |||
8244c2340a | |||
28080595d0 | |||
3999aa96fb | |||
b5a8957720 | |||
9b01213990 | |||
ae64d9f0fd | |||
ea55083929 | |||
786c38b4cc | |||
60521d89cb | |||
7e7fc75e77 | |||
d0d46299d2 | |||
e025eabdef | |||
44238e6372 | |||
be986c8474 | |||
afb3623622 | |||
5eb6d62c9c | |||
2c802cad63 |
4
.github/actions/setup/action.yml
vendored
4
.github/actions/setup/action.yml
vendored
@ -30,6 +30,10 @@ runs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup docker cache
|
||||
uses: ScribeMD/docker-cache@0.5.0
|
||||
with:
|
||||
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
||||
- name: Setup dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
|
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -11,7 +11,7 @@ services:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
image: docker.io/library/redis
|
||||
image: docker.io/library/redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: always
|
||||
|
31
.github/codespell-words.txt
vendored
31
.github/codespell-words.txt
vendored
@ -1,7 +1,32 @@
|
||||
akadmin
|
||||
asgi
|
||||
assertIn
|
||||
authentik
|
||||
authn
|
||||
crate
|
||||
docstrings
|
||||
entra
|
||||
goauthentik
|
||||
gunicorn
|
||||
hass
|
||||
jwe
|
||||
jwks
|
||||
keypair
|
||||
keypairs
|
||||
hass
|
||||
warmup
|
||||
kubernetes
|
||||
oidc
|
||||
ontext
|
||||
openid
|
||||
passwordless
|
||||
plex
|
||||
saml
|
||||
scim
|
||||
singed
|
||||
assertIn
|
||||
slo
|
||||
sso
|
||||
totp
|
||||
traefik
|
||||
# https://github.com/codespell-project/codespell/issues/1224
|
||||
upToDate
|
||||
warmup
|
||||
webauthn
|
||||
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -82,6 +82,12 @@ updates:
|
||||
docusaurus:
|
||||
patterns:
|
||||
- "@docusaurus/*"
|
||||
build:
|
||||
patterns:
|
||||
- "@swc/*"
|
||||
- "swc-*"
|
||||
- "lightningcss*"
|
||||
- "@rspack/binding*"
|
||||
- package-ecosystem: npm
|
||||
directory: "/lifecycle/aws"
|
||||
schedule:
|
||||
|
@ -40,7 +40,7 @@ jobs:
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3.5.0
|
||||
- uses: docker/setup-qemu-action@v3.6.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
|
2
.github/workflows/ci-main-daily.yml
vendored
2
.github/workflows/ci-main-daily.yml
vendored
@ -15,8 +15,8 @@ jobs:
|
||||
matrix:
|
||||
version:
|
||||
- docs
|
||||
- version-2025-2
|
||||
- version-2024-12
|
||||
- version-2024-10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.5.0
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
|
4
.github/workflows/release-publish.yml
vendored
4
.github/workflows/release-publish.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.5.0
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
@ -186,7 +186,7 @@ jobs:
|
||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||
docker cp ${container}:web/ .
|
||||
- name: Create a Sentry.io release
|
||||
uses: getsentry/action-release@v1
|
||||
uses: getsentry/action-release@v3
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
@ -32,6 +32,8 @@ jobs:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: run extract
|
||||
run: |
|
||||
poetry run make i18n-extract
|
||||
|
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@ -1,26 +1,4 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"akadmin",
|
||||
"asgi",
|
||||
"authentik",
|
||||
"authn",
|
||||
"entra",
|
||||
"goauthentik",
|
||||
"jwe",
|
||||
"jwks",
|
||||
"kubernetes",
|
||||
"oidc",
|
||||
"openid",
|
||||
"passwordless",
|
||||
"plex",
|
||||
"saml",
|
||||
"scim",
|
||||
"slo",
|
||||
"sso",
|
||||
"totp",
|
||||
"traefik",
|
||||
"webauthn"
|
||||
],
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"yaml.customTags": [
|
||||
|
@ -5,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
identity and expression, level of experience, education, socioeconomic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
|
@ -3,7 +3,8 @@
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_ENV=production \
|
||||
GIT_UNAVAILABLE=true
|
||||
|
||||
WORKDIR /work/website
|
||||
|
||||
|
67
Makefile
67
Makefile
@ -4,34 +4,17 @@
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
PY_SOURCES = authentik tests scripts lifecycle .github
|
||||
GO_SOURCES = cmd internal
|
||||
WEB_SOURCES = web/src web/packages
|
||||
DOCKER_IMAGE ?= "authentik:test"
|
||||
|
||||
GEN_API_TS = "gen-ts-api"
|
||||
GEN_API_PY = "gen-py-api"
|
||||
GEN_API_GO = "gen-go-api"
|
||||
|
||||
pg_user := $(shell python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||
pg_host := $(shell python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||
pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
|
||||
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
||||
-I .github/codespell-words.txt \
|
||||
-S 'web/src/locales/**' \
|
||||
-S 'website/docs/developer-docs/api/reference/**' \
|
||||
-S '**/node_modules/**' \
|
||||
-S '**/dist/**' \
|
||||
$(PY_SOURCES) \
|
||||
$(GO_SOURCES) \
|
||||
$(WEB_SOURCES) \
|
||||
website/src \
|
||||
website/blog \
|
||||
website/docs \
|
||||
website/integrations \
|
||||
website/src
|
||||
pg_user := $(shell poetry run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||
pg_host := $(shell poetry run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||
pg_name := $(shell poetry run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
|
||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
||||
|
||||
@ -49,26 +32,26 @@ go-test:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
coverage run manage.py test --keepdb authentik
|
||||
coverage html
|
||||
coverage report
|
||||
poetry run coverage run manage.py test --keepdb authentik
|
||||
poetry run coverage html
|
||||
poetry run coverage report
|
||||
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
black $(PY_SOURCES)
|
||||
ruff check --fix $(PY_SOURCES)
|
||||
poetry run black $(PY_SOURCES)
|
||||
poetry run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
codespell -w $(CODESPELL_ARGS)
|
||||
poetry run codespell -w
|
||||
|
||||
lint: ## Lint the python and golang sources
|
||||
bandit -r $(PY_SOURCES) -x web/node_modules -x tests/wdio/node_modules -x website/node_modules
|
||||
poetry run bandit -c pyproject.toml -r $(PY_SOURCES)
|
||||
golangci-lint run -v
|
||||
|
||||
core-install:
|
||||
poetry install
|
||||
|
||||
migrate: ## Run the Authentik Django server's migrations
|
||||
python -m lifecycle.migrate
|
||||
poetry run python -m lifecycle.migrate
|
||||
|
||||
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
||||
|
||||
@ -76,7 +59,7 @@ aws-cfn:
|
||||
cd lifecycle/aws && npm run aws-cfn
|
||||
|
||||
core-i18n-extract:
|
||||
ak makemessages \
|
||||
poetry run ak makemessages \
|
||||
--add-location file \
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
@ -107,11 +90,11 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
ak make_blueprint_schema > blueprints/schema.json
|
||||
poetry run ak make_blueprint_schema > blueprints/schema.json
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
ak spectacular --file schema.yml
|
||||
poetry run ak spectacular --file schema.yml
|
||||
|
||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||
@ -162,7 +145,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
docker.io/openapitools/openapi-generator-cli:v7.4.0 generate \
|
||||
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g python \
|
||||
-o /local/${GEN_API_PY} \
|
||||
@ -190,7 +173,7 @@ gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
||||
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
||||
|
||||
gen-dev-config: ## Generate a local development config file
|
||||
python -m scripts.generate_config
|
||||
poetry run scripts/generate_config.py
|
||||
|
||||
gen: gen-build gen-client-ts
|
||||
|
||||
@ -271,21 +254,21 @@ ci--meta-debug:
|
||||
node --version
|
||||
|
||||
ci-black: ci--meta-debug
|
||||
black --check $(PY_SOURCES)
|
||||
poetry run black --check $(PY_SOURCES)
|
||||
|
||||
ci-ruff: ci--meta-debug
|
||||
ruff check $(PY_SOURCES)
|
||||
poetry run ruff check $(PY_SOURCES)
|
||||
|
||||
ci-codespell: ci--meta-debug
|
||||
codespell $(CODESPELL_ARGS) -s
|
||||
poetry run codespell -s
|
||||
|
||||
ci-bandit: ci--meta-debug
|
||||
bandit -r $(PY_SOURCES)
|
||||
poetry run bandit -r $(PY_SOURCES)
|
||||
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
ak makemigrations --check
|
||||
poetry run ak makemigrations --check
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
coverage report
|
||||
coverage xml
|
||||
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
poetry run coverage report
|
||||
poetry run coverage xml
|
||||
|
@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
|
||||
|
||||
## Independent audits and pentests
|
||||
|
||||
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
||||
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specific audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
||||
|
||||
## What authentik classifies as a CVE
|
||||
|
||||
|
@ -5,6 +5,7 @@ from collections.abc import Iterable
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
@ -154,6 +155,17 @@ class SourceViewSet(
|
||||
matching_sources.append(source_settings.validated_data)
|
||||
return Response(matching_sources)
|
||||
|
||||
def destroy(self, request: Request, *args, **kwargs):
|
||||
"""Prevent deletion of built-in sources"""
|
||||
instance: Source = self.get_object()
|
||||
|
||||
if instance.managed == Source.MANAGED_INBUILT:
|
||||
raise ValidationError(
|
||||
{"detail": "Built-in sources cannot be deleted"}, code="protected"
|
||||
)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"""User source connection"""
|
||||
|
@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
||||
"name": "authentik Built-in",
|
||||
"slug": "authentik-built-in",
|
||||
},
|
||||
managed="goauthentik.io/sources/inbuilt",
|
||||
managed=Source.MANAGED_INBUILT,
|
||||
)
|
||||
|
@ -678,6 +678,8 @@ class SourceGroupMatchingModes(models.TextChoices):
|
||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
|
||||
|
||||
name = models.TextField(help_text=_("Source's display Name."))
|
||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||
|
||||
|
@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise Http404 from None
|
||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||
plan.append_stage(in_memory_stage(RedirectToAppStage))
|
||||
return plan.to_redirect(request, flow)
|
||||
|
||||
|
||||
|
@ -37,6 +37,7 @@ class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSeriali
|
||||
"user_delete_action",
|
||||
"group_delete_action",
|
||||
"default_group_email_domain",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
|
@ -8,9 +8,10 @@ from httplib2 import HttpLib2Error, HttpLib2ErrorWithResponse
|
||||
|
||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
BadRequestSyncException,
|
||||
DryRunRejected,
|
||||
NotFoundSyncException,
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
@ -43,6 +44,8 @@ class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict
|
||||
self.domains.append(domain_name)
|
||||
|
||||
def _request(self, request: HttpRequest):
|
||||
if self.provider.dry_run and request.method.upper() not in SAFE_METHODS:
|
||||
raise DryRunRejected(request.uri, request.method, request.body)
|
||||
try:
|
||||
response = request.execute()
|
||||
except GoogleAuthError as exc:
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-24 19:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_providers_google_workspace",
|
||||
"0003_googleworkspaceprovidergroup_attributes_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="googleworkspaceprovider",
|
||||
name="dry_run",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, provider will not modify or create objects in the remote system.",
|
||||
),
|
||||
),
|
||||
]
|
@ -36,6 +36,7 @@ class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializ
|
||||
"filter_group",
|
||||
"user_delete_action",
|
||||
"group_delete_action",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
|
@ -3,6 +3,7 @@ from collections.abc import Coroutine
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from azure.core.exceptions import (
|
||||
ClientAuthenticationError,
|
||||
ServiceRequestError,
|
||||
@ -12,6 +13,7 @@ from azure.identity.aio import ClientSecretCredential
|
||||
from django.db.models import Model
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
from kiota_abstractions.api_error import APIError
|
||||
from kiota_abstractions.request_information import RequestInformation
|
||||
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
||||
AzureIdentityAuthenticationProvider,
|
||||
)
|
||||
@ -21,13 +23,15 @@ from msgraph.generated.models.o_data_errors.o_data_error import ODataError
|
||||
from msgraph.graph_request_adapter import GraphRequestAdapter, options
|
||||
from msgraph.graph_service_client import GraphServiceClient
|
||||
from msgraph_core import GraphClientFactory
|
||||
from opentelemetry import trace
|
||||
|
||||
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
BadRequestSyncException,
|
||||
DryRunRejected,
|
||||
NotFoundSyncException,
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
@ -35,20 +39,24 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
)
|
||||
|
||||
|
||||
def get_request_adapter(
|
||||
credentials: ClientSecretCredential, scopes: list[str] | None = None
|
||||
) -> GraphRequestAdapter:
|
||||
if scopes:
|
||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
|
||||
else:
|
||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
|
||||
class AuthentikRequestAdapter(GraphRequestAdapter):
|
||||
def __init__(self, auth_provider, provider: MicrosoftEntraProvider, client=None):
|
||||
super().__init__(auth_provider, client)
|
||||
self._provider = provider
|
||||
|
||||
return GraphRequestAdapter(
|
||||
auth_provider=auth_provider,
|
||||
client=GraphClientFactory.create_with_default_middleware(
|
||||
options=options, client=KiotaClientFactory.get_default_client()
|
||||
),
|
||||
)
|
||||
async def get_http_response_message(
|
||||
self,
|
||||
request_info: RequestInformation,
|
||||
parent_span: trace.Span,
|
||||
claims: str = "",
|
||||
) -> httpx.Response:
|
||||
if self._provider.dry_run and request_info.http_method.value.upper() not in SAFE_METHODS:
|
||||
raise DryRunRejected(
|
||||
url=request_info.url,
|
||||
method=request_info.http_method.value,
|
||||
body=request_info.content.decode("utf-8"),
|
||||
)
|
||||
return await super().get_http_response_message(request_info, parent_span, claims=claims)
|
||||
|
||||
|
||||
class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
|
||||
@ -63,9 +71,27 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
|
||||
self.credentials = provider.microsoft_credentials()
|
||||
self.__prefetch_domains()
|
||||
|
||||
def get_request_adapter(
|
||||
self, credentials: ClientSecretCredential, scopes: list[str] | None = None
|
||||
) -> AuthentikRequestAdapter:
|
||||
if scopes:
|
||||
auth_provider = AzureIdentityAuthenticationProvider(
|
||||
credentials=credentials, scopes=scopes
|
||||
)
|
||||
else:
|
||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
|
||||
|
||||
return AuthentikRequestAdapter(
|
||||
auth_provider=auth_provider,
|
||||
provider=self.provider,
|
||||
client=GraphClientFactory.create_with_default_middleware(
|
||||
options=options, client=KiotaClientFactory.get_default_client()
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return GraphServiceClient(request_adapter=get_request_adapter(**self.credentials))
|
||||
return GraphServiceClient(request_adapter=self.get_request_adapter(**self.credentials))
|
||||
|
||||
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
|
||||
try:
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-24 19:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_providers_microsoft_entra",
|
||||
"0002_microsoftentraprovidergroup_attributes_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="microsoftentraprovider",
|
||||
name="dry_run",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, provider will not modify or create objects in the remote system.",
|
||||
),
|
||||
),
|
||||
]
|
@ -32,7 +32,6 @@ class MicrosoftEntraUserTests(APITestCase):
|
||||
|
||||
@apply_blueprint("system/providers-microsoft-entra.yaml")
|
||||
def setUp(self) -> None:
|
||||
|
||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||
# which will cause errors with multiple users
|
||||
Tenant.objects.update(avatars="none")
|
||||
@ -97,6 +96,38 @@ class MicrosoftEntraUserTests(APITestCase):
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_called_once()
|
||||
|
||||
def test_user_create_dry_run(self):
|
||||
"""Test user creation (dry run)"""
|
||||
self.provider.dry_run = True
|
||||
self.provider.save()
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNone(microsoft_user)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
|
||||
def test_user_not_created(self):
|
||||
"""Test without property mappings, no group is created"""
|
||||
self.provider.property_mappings.clear()
|
||||
|
@ -89,9 +89,9 @@ class SourceStageFinal(StageView):
|
||||
This stage uses the override flow token to resume execution of the initial flow the
|
||||
source stage is bound to."""
|
||||
|
||||
def dispatch(self):
|
||||
def dispatch(self, *args, **kwargs):
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
response = plan.to_redirect(self.request, token.flow)
|
||||
|
@ -4,7 +4,8 @@ from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
||||
from authentik.enterprise.stages.source.stage import SourceStageFinal
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
@ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase):
|
||||
self.assertIsNotNone(flow_token)
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase):
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.assertStageRedirects(
|
||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
|
@ -76,10 +76,10 @@ class FlowPlan:
|
||||
self.bindings.append(binding)
|
||||
self.markers.append(marker or StageMarker())
|
||||
|
||||
def insert_stage(self, stage: Stage, marker: StageMarker | None = None):
|
||||
def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1):
|
||||
"""Insert stage into plan, as immediate next stage"""
|
||||
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
|
||||
self.markers.insert(1, marker or StageMarker())
|
||||
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
|
||||
self.markers.insert(index, marker or StageMarker())
|
||||
|
||||
def redirect(self, destination: str):
|
||||
"""Insert a redirect stage as next stage"""
|
||||
|
@ -282,16 +282,14 @@ class ConfigLoader:
|
||||
|
||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||
"""Wrapper for get that converts value into int or None if set"""
|
||||
value = self.get(path, default)
|
||||
value = self.get(path, UNSET)
|
||||
if value is UNSET:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError) as exc:
|
||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||
return default
|
||||
if value is UNSET:
|
||||
return default
|
||||
return None
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
@ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
"sslcert": config.get("postgresql.sslcert"),
|
||||
"sslkey": config.get("postgresql.sslkey"),
|
||||
},
|
||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
||||
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
|
||||
"postgresql.disable_server_side_cursors", False
|
||||
),
|
||||
"TEST": {
|
||||
@ -383,8 +381,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
}
|
||||
}
|
||||
|
||||
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
if config.get_bool("postgresql.use_pgpool", False):
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
|
@ -33,6 +33,7 @@ class SyncObjectSerializer(PassiveSerializer):
|
||||
)
|
||||
)
|
||||
sync_object_id = CharField()
|
||||
override_dry_run = BooleanField(default=False)
|
||||
|
||||
|
||||
class SyncObjectResultSerializer(PassiveSerializer):
|
||||
@ -98,6 +99,7 @@ class OutgoingSyncProviderStatusMixin:
|
||||
page=1,
|
||||
provider_pk=provider.pk,
|
||||
pk=params.validated_data["sync_object_id"],
|
||||
override_dry_run=params.validated_data["override_dry_run"],
|
||||
).get()
|
||||
return Response(SyncObjectResultSerializer(instance={"messages": res}).data)
|
||||
|
||||
|
@ -28,6 +28,14 @@ class Direction(StrEnum):
|
||||
remove = "remove"
|
||||
|
||||
|
||||
SAFE_METHODS = [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"TRACE",
|
||||
]
|
||||
|
||||
|
||||
class BaseOutgoingSyncClient[
|
||||
TModel: "Model", TConnection: "Model", TSchema: dict, TProvider: "OutgoingSyncProvider"
|
||||
]:
|
||||
|
@ -21,6 +21,22 @@ class BadRequestSyncException(BaseSyncException):
|
||||
"""Exception when invalid data was sent to the remote system"""
|
||||
|
||||
|
||||
class DryRunRejected(BaseSyncException):
|
||||
"""When dry_run is enabled and a provider dropped a mutating request"""
|
||||
|
||||
def __init__(self, url: str, method: str, body: dict):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.body = body
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return f"Dry-run rejected request: {self.method} {self.url}"
|
||||
|
||||
|
||||
class StopSync(BaseSyncException):
|
||||
"""Exception raised when a configuration error should stop the sync process"""
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
from typing import Any, Self
|
||||
|
||||
import pglock
|
||||
from django.db import connection
|
||||
from django.db import connection, models
|
||||
from django.db.models import Model, QuerySet, TextChoices
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
@ -18,6 +19,14 @@ class OutgoingSyncDeleteAction(TextChoices):
|
||||
|
||||
|
||||
class OutgoingSyncProvider(Model):
|
||||
"""Base abstract models for providers implementing outgoing sync"""
|
||||
|
||||
dry_run = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"When enabled, provider will not modify or create objects in the remote system."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -32,7 +41,7 @@ class OutgoingSyncProvider(Model):
|
||||
|
||||
@property
|
||||
def sync_lock(self) -> pglock.advisory:
|
||||
"""Postgres lock for syncing SCIM to prevent multiple parallel syncs happening"""
|
||||
"""Postgres lock for syncing to prevent multiple parallel syncs happening"""
|
||||
return pglock.advisory(
|
||||
lock_id=f"goauthentik.io/{connection.schema_name}/providers/outgoing-sync/{str(self.pk)}",
|
||||
timeout=0,
|
||||
|
@ -20,6 +20,7 @@ from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
|
||||
from authentik.lib.sync.outgoing.base import Direction
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
BadRequestSyncException,
|
||||
DryRunRejected,
|
||||
StopSync,
|
||||
TransientSyncException,
|
||||
)
|
||||
@ -105,7 +106,9 @@ class SyncTasks:
|
||||
return
|
||||
task.set_status(TaskStatus.SUCCESSFUL, *messages)
|
||||
|
||||
def sync_objects(self, object_type: str, page: int, provider_pk: int, **filter):
|
||||
def sync_objects(
|
||||
self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter
|
||||
):
|
||||
_object_type = path_to_class(object_type)
|
||||
self.logger = get_logger().bind(
|
||||
provider_type=class_to_path(self._provider_model),
|
||||
@ -116,6 +119,10 @@ class SyncTasks:
|
||||
provider = self._provider_model.objects.filter(pk=provider_pk).first()
|
||||
if not provider:
|
||||
return messages
|
||||
# Override dry run mode if requested, however don't save the provider
|
||||
# so that scheduled sync tasks still run in dry_run mode
|
||||
if override_dry_run:
|
||||
provider.dry_run = False
|
||||
try:
|
||||
client = provider.client_for_model(_object_type)
|
||||
except TransientSyncException:
|
||||
@ -132,6 +139,22 @@ class SyncTasks:
|
||||
except SkipObjectException:
|
||||
self.logger.debug("skipping object due to SkipObject", obj=obj)
|
||||
continue
|
||||
except DryRunRejected as exc:
|
||||
messages.append(
|
||||
asdict(
|
||||
LogEvent(
|
||||
_("Dropping mutating request due to dry run"),
|
||||
log_level="info",
|
||||
logger=f"{provider._meta.verbose_name}@{object_type}",
|
||||
attributes={
|
||||
"obj": sanitize_item(obj),
|
||||
"method": exc.method,
|
||||
"url": exc.url,
|
||||
"body": exc.body,
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
except BadRequestSyncException as exc:
|
||||
self.logger.warning("failed to sync object", exc=exc, obj=obj)
|
||||
messages.append(
|
||||
@ -231,8 +254,10 @@ class SyncTasks:
|
||||
raise Retry() from exc
|
||||
except SkipObjectException:
|
||||
continue
|
||||
except DryRunRejected as exc:
|
||||
self.logger.info("Rejected dry-run event", exc=exc)
|
||||
except StopSync as exc:
|
||||
self.logger.warning(exc, provider_pk=provider.pk)
|
||||
self.logger.warning("Stopping sync", exc=exc, provider_pk=provider.pk)
|
||||
|
||||
def sync_signal_m2m(self, group_pk: str, action: str, pk_set: list[int]):
|
||||
self.logger = get_logger().bind(
|
||||
@ -263,5 +288,7 @@ class SyncTasks:
|
||||
raise Retry() from exc
|
||||
except SkipObjectException:
|
||||
continue
|
||||
except DryRunRejected as exc:
|
||||
self.logger.info("Rejected dry-run event", exc=exc)
|
||||
except StopSync as exc:
|
||||
self.logger.warning(exc, provider_pk=provider.pk)
|
||||
self.logger.warning("Stopping sync", exc=exc, provider_pk=provider.pk)
|
||||
|
@ -158,6 +158,18 @@ class TestConfig(TestCase):
|
||||
test_obj = Test()
|
||||
dumps(test_obj, indent=4, cls=AttrEncoder)
|
||||
|
||||
def test_get_optional_int(self):
|
||||
config = ConfigLoader()
|
||||
self.assertEqual(config.get_optional_int("foo", 21), 21)
|
||||
self.assertEqual(config.get_optional_int("foo"), None)
|
||||
config.set("foo", "21")
|
||||
self.assertEqual(config.get_optional_int("foo"), 21)
|
||||
self.assertEqual(config.get_optional_int("foo", 0), 21)
|
||||
self.assertEqual(config.get_optional_int("foo", "null"), 21)
|
||||
config.set("foo", "null")
|
||||
self.assertEqual(config.get_optional_int("foo"), None)
|
||||
self.assertEqual(config.get_optional_int("foo", 21), None)
|
||||
|
||||
@mock.patch.dict(environ, check_deprecations_env_vars)
|
||||
def test_check_deprecations(self):
|
||||
"""Test config key re-write for deprecated env vars"""
|
||||
@ -221,6 +233,16 @@ class TestConfig(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_db_conn_max_age(self):
|
||||
"""Test DB conn_max_age Config"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.conn_max_age", "null")
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf["default"]["CONN_MAX_AGE"],
|
||||
None,
|
||||
)
|
||||
|
||||
def test_db_read_replicas(self):
|
||||
"""Test read replicas"""
|
||||
config = ConfigLoader()
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Base Kubernetes Reconciler"""
|
||||
|
||||
import re
|
||||
from dataclasses import asdict
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
@ -67,7 +68,8 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the name of the object this reconciler manages"""
|
||||
return (
|
||||
|
||||
base_name = (
|
||||
self.controller.outpost.config.object_naming_template
|
||||
% {
|
||||
"name": slugify(self.controller.outpost.name),
|
||||
@ -75,6 +77,16 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
}
|
||||
).lower()
|
||||
|
||||
formatted = slugify(base_name)
|
||||
formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
|
||||
formatted = re.sub(r"-+", "-", formatted)
|
||||
formatted = formatted[:63]
|
||||
|
||||
if not formatted:
|
||||
formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
|
||||
|
||||
return formatted
|
||||
|
||||
def get_patched_reference_object(self) -> T:
|
||||
"""Get patched reference object"""
|
||||
reference = self.get_reference_object()
|
||||
@ -112,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
try:
|
||||
current = self.retrieve()
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
|
||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||
self.logger.debug("Failed to get current, triggering recreate")
|
||||
raise NeedsRecreate from exc
|
||||
@ -156,7 +167,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
self.delete(current)
|
||||
self.logger.debug("Removing")
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
|
||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||
self.logger.debug("Failed to get current, assuming non-existent")
|
||||
return
|
||||
|
@ -61,9 +61,14 @@ class KubernetesController(BaseController):
|
||||
client: KubernetesClient
|
||||
connection: KubernetesServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
outpost: Outpost,
|
||||
connection: KubernetesServiceConnection,
|
||||
client: KubernetesClient | None = None,
|
||||
) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
self.client = KubernetesClient(connection)
|
||||
self.client = client if client else KubernetesClient(connection)
|
||||
self.reconcilers = {
|
||||
SecretReconciler.reconciler_name(): SecretReconciler,
|
||||
DeploymentReconciler.reconciler_name(): DeploymentReconciler,
|
||||
|
44
authentik/outposts/tests/test_controller_k8s.py
Normal file
44
authentik/outposts/tests/test_controller_k8s.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Kubernetes controller tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
|
||||
|
||||
class KubernetesControllerTests(TestCase):
|
||||
"""Kubernetes controller tests"""
|
||||
|
||||
@reconcile_app("authentik_outposts")
|
||||
def setUp(self) -> None:
|
||||
self.outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
self.integration = KubernetesServiceConnection(name="test")
|
||||
|
||||
def test_gen_name(self):
|
||||
"""Ensure the generated name is valid"""
|
||||
controller = KubernetesController(
|
||||
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(),
|
||||
self.integration,
|
||||
# Pass something not-none as client so we don't
|
||||
# attempt to connect to K8s as that's not needed
|
||||
client=self,
|
||||
)
|
||||
rec = DeploymentReconciler(controller)
|
||||
self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost")
|
||||
|
||||
controller.outpost.name = generate_id()
|
||||
self.assertLess(len(rec.name), 64)
|
||||
|
||||
# Test custom naming template
|
||||
_cfg = controller.outpost.config
|
||||
_cfg.object_naming_template = ""
|
||||
controller.outpost.config = _cfg
|
||||
self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}")
|
||||
self.assertLess(len(rec.name), 64)
|
@ -9,7 +9,12 @@ from hashlib import sha256
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
SECP256R1,
|
||||
SECP384R1,
|
||||
SECP521R1,
|
||||
EllipticCurvePrivateKey,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
from dacite import Config
|
||||
@ -114,6 +119,22 @@ class JWTAlgorithms(models.TextChoices):
|
||||
HS256 = "HS256", _("HS256 (Symmetric Encryption)")
|
||||
RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
|
||||
ES256 = "ES256", _("ES256 (Asymmetric Encryption)")
|
||||
ES384 = "ES384", _("ES384 (Asymmetric Encryption)")
|
||||
ES512 = "ES512", _("ES512 (Asymmetric Encryption)")
|
||||
|
||||
@classmethod
|
||||
def from_private_key(cls, private_key: PrivateKeyTypes | None) -> str:
|
||||
if isinstance(private_key, RSAPrivateKey):
|
||||
return cls.RS256
|
||||
if isinstance(private_key, EllipticCurvePrivateKey):
|
||||
curve = private_key.curve
|
||||
if isinstance(curve, SECP256R1):
|
||||
return cls.ES256
|
||||
if isinstance(curve, SECP384R1):
|
||||
return cls.ES384
|
||||
if isinstance(curve, SECP521R1):
|
||||
return cls.ES512
|
||||
raise ValueError(f"Invalid private key type: {type(private_key)}")
|
||||
|
||||
|
||||
class ScopeMapping(PropertyMapping):
|
||||
@ -263,11 +284,7 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
return self.client_secret, JWTAlgorithms.HS256
|
||||
key: CertificateKeyPair = self.signing_key
|
||||
private_key = key.private_key
|
||||
if isinstance(private_key, RSAPrivateKey):
|
||||
return private_key, JWTAlgorithms.RS256
|
||||
if isinstance(private_key, EllipticCurvePrivateKey):
|
||||
return private_key, JWTAlgorithms.ES256
|
||||
raise ValueError(f"Invalid private key type: {type(private_key)}")
|
||||
return private_key, JWTAlgorithms.from_private_key(private_key)
|
||||
|
||||
def get_issuer(self, request: HttpRequest) -> str | None:
|
||||
"""Get issuer, based on request"""
|
||||
|
@ -254,10 +254,10 @@ class OAuthAuthorizationParams:
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
||||
if SCOPE_OFFLINE_ACCESS in self.scope:
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
if PROMPT_CONSENT not in self.prompt:
|
||||
# Instead of ignoring the `offline_access` scope when `prompt`
|
||||
# isn't set to `consent`, we set override it ourselves
|
||||
self.prompt.add(PROMPT_CONSENT)
|
||||
# Don't explicitly request consent with offline_access, as the spec allows for
|
||||
# "other conditions for processing the request permitting offline access to the
|
||||
# requested resources are in place"
|
||||
# which we interpret as "the admin picks an authorization flow with or without consent"
|
||||
if self.response_type not in [
|
||||
ResponseTypes.CODE,
|
||||
ResponseTypes.CODE_TOKEN,
|
||||
|
@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
|
||||
except FlowNonApplicableException:
|
||||
LOGGER.warning("Flow not applicable to user")
|
||||
return None
|
||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
|
||||
|
||||
|
||||
|
@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(self.request, self.flow)
|
||||
|
@ -75,10 +75,7 @@ class JWKSView(View):
|
||||
key_data = {}
|
||||
|
||||
if use == "sig":
|
||||
if isinstance(private_key, RSAPrivateKey):
|
||||
key_data["alg"] = JWTAlgorithms.RS256
|
||||
elif isinstance(private_key, EllipticCurvePrivateKey):
|
||||
key_data["alg"] = JWTAlgorithms.ES256
|
||||
key_data["alg"] = JWTAlgorithms.from_private_key(private_key)
|
||||
elif use == "enc":
|
||||
key_data["alg"] = "RSA-OAEP-256"
|
||||
key_data["enc"] = "A256CBC-HS512"
|
||||
|
@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
def reconciler_name() -> str:
|
||||
return "ingress"
|
||||
|
||||
def _check_annotations(self, reference: V1Ingress):
|
||||
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
|
||||
"""Check that all annotations *we* set are correct"""
|
||||
for key, value in self.get_ingress_annotations().items():
|
||||
if key not in reference.metadata.annotations:
|
||||
for key, value in reference.metadata.annotations.items():
|
||||
if key not in current.metadata.annotations:
|
||||
raise NeedsUpdate()
|
||||
if reference.metadata.annotations[key] != value:
|
||||
if current.metadata.annotations[key] != value:
|
||||
raise NeedsUpdate()
|
||||
|
||||
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
||||
super().reconcile(current, reference)
|
||||
self._check_annotations(reference)
|
||||
self._check_annotations(current, reference)
|
||||
# Create a list of all expected host and tls hosts
|
||||
expected_hosts = []
|
||||
expected_hosts_tls = []
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""RAC app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikProviderRAC(AppConfig):
|
||||
class AuthentikProviderRAC(ManagedAppConfig):
|
||||
"""authentik rac app config"""
|
||||
|
||||
name = "authentik.providers.rac"
|
||||
|
@ -4,8 +4,7 @@ from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
|
||||
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Endpoint)
|
||||
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
||||
"""Clear user's endpoint cache upon endpoint creation"""
|
||||
if not created: # pragma: no cover
|
||||
return
|
||||
|
||||
# Delete user endpoint cache
|
||||
@receiver([post_save, post_delete], sender=Endpoint)
|
||||
def post_save_post_delete_endpoint(**_):
|
||||
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
|
@ -46,7 +46,7 @@ class RACStartView(PolicyAccessView):
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise Http404 from None
|
||||
plan.insert_stage(
|
||||
plan.append_stage(
|
||||
in_memory_stage(
|
||||
RACFinalStage,
|
||||
application=self.application,
|
||||
|
@ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView):
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(self.request, self.flow)
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
|
@ -28,8 +28,10 @@ class SCIMProviderSerializer(ProviderSerializer):
|
||||
"url",
|
||||
"verify_certificates",
|
||||
"token",
|
||||
"compatibility_mode",
|
||||
"exclude_users_service_account",
|
||||
"filter_group",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
|
@ -12,8 +12,9 @@ from authentik.lib.sync.outgoing import (
|
||||
HTTP_SERVICE_UNAVAILABLE,
|
||||
HTTP_TOO_MANY_REQUESTS,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
DryRunRejected,
|
||||
NotFoundSyncException,
|
||||
ObjectExistsSyncException,
|
||||
TransientSyncException,
|
||||
@ -21,7 +22,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.models import SCIMCompatibilityMode, SCIMProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import Model
|
||||
@ -54,6 +55,8 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs) -> dict:
|
||||
"""Wrapper to send a request to the full URL"""
|
||||
if self.provider.dry_run and method.upper() not in SAFE_METHODS:
|
||||
raise DryRunRejected(f"{self.base_url}{path}", method, body=kwargs.get("json"))
|
||||
try:
|
||||
response = self._session.request(
|
||||
method,
|
||||
@ -87,9 +90,14 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
"""Get Service provider config"""
|
||||
default_config = ServiceProviderConfiguration.default()
|
||||
try:
|
||||
return ServiceProviderConfiguration.model_validate(
|
||||
config = ServiceProviderConfiguration.model_validate(
|
||||
self._request("GET", "/ServiceProviderConfig")
|
||||
)
|
||||
if self.provider.compatibility_mode == SCIMCompatibilityMode.AWS:
|
||||
config.patch.supported = False
|
||||
if self.provider.compatibility_mode == SCIMCompatibilityMode.SLACK:
|
||||
config.filter.supported = True
|
||||
return config
|
||||
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
|
||||
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
||||
return default_config
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""User client"""
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils.http import urlencode
|
||||
from pydantic import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync
|
||||
from authentik.policies.utils import delete_none_values
|
||||
from authentik.providers.scim.clients.base import SCIMClient
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
@ -55,18 +57,35 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
def create(self, user: User):
|
||||
"""Create user from scratch and create a connection object"""
|
||||
scim_user = self.to_schema(user, None)
|
||||
response = self._request(
|
||||
"POST",
|
||||
"/Users",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
|
||||
with transaction.atomic():
|
||||
try:
|
||||
response = self._request(
|
||||
"POST",
|
||||
"/Users",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
except ObjectExistsSyncException as exc:
|
||||
if not self._config.filter.supported:
|
||||
raise exc
|
||||
users = self._request(
|
||||
"GET", f"/Users?{urlencode({'filter': f'userName eq {scim_user.userName}'})}"
|
||||
)
|
||||
users_res = users.get("Resources", [])
|
||||
if len(users_res) < 1:
|
||||
raise exc
|
||||
return SCIMProviderUser.objects.create(
|
||||
provider=self.provider, user=user, scim_id=users_res[0]["id"]
|
||||
)
|
||||
else:
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
return SCIMProviderUser.objects.create(
|
||||
provider=self.provider, user=user, scim_id=scim_id
|
||||
)
|
||||
|
||||
def update(self, user: User, connection: SCIMProviderUser):
|
||||
"""Update existing user"""
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-24 19:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_scim", "0010_scimprovider_verify_certificates"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="dry_run",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, provider will not modify or create objects in the remote system.",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.12 on 2025-03-07 23:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_scim", "0011_scimprovider_dry_run"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="compatibility_mode",
|
||||
field=models.CharField(
|
||||
choices=[("default", "Default"), ("aws", "AWS"), ("slack", "Slack")],
|
||||
default="default",
|
||||
help_text="Alter authentik behavior for vendor-specific SCIM implementations.",
|
||||
max_length=30,
|
||||
verbose_name="SCIM Compatibility Mode",
|
||||
),
|
||||
),
|
||||
]
|
@ -57,6 +57,14 @@ class SCIMProviderGroup(SerializerModel):
|
||||
return f"SCIM Provider Group {self.group_id} to {self.provider_id}"
|
||||
|
||||
|
||||
class SCIMCompatibilityMode(models.TextChoices):
|
||||
"""SCIM compatibility mode"""
|
||||
|
||||
DEFAULT = "default", _("Default")
|
||||
AWS = "aws", _("AWS")
|
||||
SLACK = "slack", _("Slack")
|
||||
|
||||
|
||||
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
"""SCIM 2.0 provider to create users and groups in external applications"""
|
||||
|
||||
@ -77,6 +85,14 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
help_text=_("Property mappings used for group creation/updating."),
|
||||
)
|
||||
|
||||
compatibility_mode = models.CharField(
|
||||
max_length=30,
|
||||
choices=SCIMCompatibilityMode.choices,
|
||||
default=SCIMCompatibilityMode.DEFAULT,
|
||||
verbose_name=_("SCIM Compatibility Mode"),
|
||||
help_text=_("Alter authentik behavior for vendor-specific SCIM implementations."),
|
||||
)
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/scim.png")
|
||||
|
@ -3,12 +3,15 @@
|
||||
from json import loads
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.text import slugify
|
||||
from jsonschema import validate
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.events.models import SystemTask
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_sync, sync_tasks
|
||||
from authentik.tenants.models import Tenant
|
||||
@ -330,3 +333,59 @@ class SCIMUserTests(TestCase):
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
|
||||
def test_user_create_dry_run(self):
|
||||
"""Test user creation (dry_run)"""
|
||||
# Update the provider before we start mocking as saving the provider triggers a full sync
|
||||
self.provider.dry_run = True
|
||||
self.provider.save()
|
||||
with Mocker() as mock:
|
||||
scim_id = generate_id()
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 1, mock.request_history)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
|
||||
def test_sync_task_dry_run(self):
|
||||
"""Test sync tasks"""
|
||||
# Update the provider before we start mocking as saving the provider triggers a full sync
|
||||
self.provider.dry_run = True
|
||||
self.provider.save()
|
||||
with Mocker() as mock:
|
||||
uid = generate_id()
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
|
||||
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
|
||||
|
||||
self.assertEqual(mock.call_count, 3)
|
||||
for request in mock.request_history:
|
||||
self.assertIn(request.method, SAFE_METHODS)
|
||||
task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first()
|
||||
self.assertIsNotNone(task)
|
||||
drop_msg = task.messages[2]
|
||||
self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run")
|
||||
self.assertIsNotNone(drop_msg["attributes"]["url"])
|
||||
self.assertIsNotNone(drop_msg["attributes"]["body"])
|
||||
self.assertIsNotNone(drop_msg["attributes"]["method"])
|
||||
|
@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
|
||||
error_desc = self.get_request_arg("error_description", None)
|
||||
return {"error": error_desc or error or _("No token received.")}
|
||||
args = {
|
||||
"client_id": self.get_client_id(),
|
||||
"client_secret": self.get_client_secret(),
|
||||
"redirect_uri": callback,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
|
@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
|
||||
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
||||
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
||||
continue
|
||||
config = well_known_config.json()
|
||||
config: dict = well_known_config.json()
|
||||
try:
|
||||
dirty = False
|
||||
source_attr_key = (
|
||||
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
|
||||
for source_attr, config_key in source_attr_key:
|
||||
# Check if we're actually changing anything to only
|
||||
# save when something has changed
|
||||
if getattr(source, source_attr, "") != config[config_key]:
|
||||
if config_key not in config:
|
||||
continue
|
||||
if getattr(source, source_attr, "") != config.get(config_key, ""):
|
||||
dirty = True
|
||||
setattr(source, source_attr, config[config_key])
|
||||
except (IndexError, KeyError) as exc:
|
||||
|
@ -25,8 +25,10 @@ class RedditOAuth2Client(UserprofileHeaderAuthClient):
|
||||
|
||||
def get_access_token(self, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret)
|
||||
return super().get_access_token(auth=auth)
|
||||
request_kwargs["auth"] = HTTPBasicAuth(
|
||||
self.source.consumer_key, self.source.consumer_secret
|
||||
)
|
||||
return super().get_access_token(**request_kwargs)
|
||||
|
||||
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
@ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
def component(self) -> str:
|
||||
return "ak-stage-authenticator-email-form"
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.friendly_name or str(self._meta.verbose_name),
|
||||
"component": "ak-user-settings-authenticator-email",
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def backend_class(self) -> type[BaseEmailBackend]:
|
||||
"""Get the email backend class to use"""
|
||||
|
@ -299,12 +299,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
data={"component": "ak-stage-authenticator-email", "code": device.token},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(device.confirmed)
|
||||
# Get a fresh session to check if the key was removed
|
||||
session = self.client.session
|
||||
session.save()
|
||||
session.load()
|
||||
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, session)
|
||||
|
||||
def test_model_properties_and_methods(self):
|
||||
"""Test model properties"""
|
||||
@ -331,7 +325,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
self.stage.send(device)
|
||||
|
||||
def test_email_tasks(self):
|
||||
|
||||
email_send_mock = MagicMock()
|
||||
with patch(
|
||||
"authentik.stages.email.tasks.send_mails",
|
||||
|
@ -146,5 +146,10 @@
|
||||
"name": "LogMeOnce",
|
||||
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjA0OCAyMDQ4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTSAwIDAgTCAtNSAxIEwgLTE1IDQgTCAtMzggMTEgTCAtNTcgMTggTCAtNzYgMjkgTCAtOTAgMzggTCAtMTA5IDUyIEwgLTExNiA1OSBMIC0xMjcgNjggTCAtMTQwIDgxIEwgLTE0OCA4OCBMIC0xNDkgOTEgTCAtMTUxIDkxIEwgLTE2MSAxMDEgTCAtMTY5IDEwOCBMIC0xNjkgMTEwIEwgLTE3MSAxMTAgTCAtMTgwIDExOSBMIC0xODggMTI2IEwgLTIwMSAxMzcgTCAtMjEyIDE0NiBMIC0yMjYgMTU3IEwgLTIzOSAxNjcgTCAtMjUwIDE3NSBMIC0yNzAgMTg4IEwgLTI5MSAyMDIgTCAtMzA5IDIxMiBMIC0zMzAgMjI0IEwgLTM1MyAyMzUgTCAtMzg2IDI1MCBMIC00MDcgMjU5IEwgLTQ0OSAyNzMgTCAtNDcyIDI4MSBMIC01MDIgMjg5IEwgLTU1MSAyOTggTCAtNjE1IDMwOSBMIC02NDcgMzE2IEwgLTY4MSAzMjQgTCAtNjk4IDMzMCBMIC03MTQgMzM4IEwgLTczNSAzNTEgTCAtNzQ1IDM1OCBMIC03NTYgMzY3IEwgLTc2NCAzNzUgTCAtNzY2IDM3OSBMIC03NjggMzc5IEwgLTc3MyAzODUgTCAtNzgxIDM5NSBMIC03OTEgNDEwIEwgLTgwMSA0MjggTCAtODEwIDQ0NyBMIC04MTYgNDY1IEwgLTgyMiA1MDAgTCAtODI3IDU0NSBMIC04MzAgNTkyIEwgLTgzMiA2NTMgTCAtODMyIDcxMiBMIC04MzEgNzQyIEwgLTgyOCA3OTQgTCAtODIyIDg2NiBMIC04MTYgOTI4IEwgLTgxMiA5NTcgTCAtODAwIDEwMjAgTCAtNzg5IDEwNzIgTCAtNzgzIDEwOTggTCAtNzY1IDExNjIgTCAtNzUxIDEyMDcgTCAtNzQ0IDEyMjUgTCAtNzM0IDEyNTQgTCAtNzIwIDEyOTAgTCAtNzA0IDEzMjggTCAtNjg5IDEzNjEgTCAtNjY4IDE0MDMgTCAtNjUzIDE0MzEgTCAtNjM5IDE0NTYgTCAtNjIwIDE0ODggTCAtNjA3IDE1MDkgTCAtNTk0IDE1MjkgTCAtNTg0IDE1NDQgTCAtNTcxIDE1NjMgTCAtNTU4IDE1ODEgTCAtNTQ0IDE2MDAgTCAtNTMzIDE2MTUgTCAtNTIwIDE2MzIgTCAtNTA3IDE2NDggTCAtNDc5IDE2ODIgTCAtNDcwIDE2OTMgTCAtNDYwIDE3MDQgTCAtNDQ5IDE3MTYgTCAtNDQyIDE3MjQgTCAtNDMyIDE3MzQgTCAtNDI1IDE3NDIgTCAtMzY4IDE3OTkgTCAtMzUxIDE4MTUgTCAtMzM1IDE4MzAgTCAtMzI3IDE4MzcgTCAtMzE0IDE4NDkgTCAtMjg4IDE4NzEgTCAtMjcxIDE4ODUgTCAtMjYxIDE4OTMgTCAtMjQ0IDE5MDcgTCAtMjI4IDE5MTkgTCAtMjE1IDE5MjkgTCAtMTk2IDE5NDMgTCAtMTg0IDE5NTIgTCAtMTY4IDE5NjMgTCAtMTQ4IDE5NzcgTCAtMTMzIDE5ODcgTCAtMTEyIDIwMDAgTCAtODggMjAxMyBMIC03MiAyMDIxIEwgLTUyIDIwMzAgTCAtMzIgMjAzNyBMIDEyIDIwNDggTCA2MCAyMDQ4IEwgNjEgMjA0NyBMIDczIDIwNDQgTCAxMDAgMjAzNyBMIDEyMSAyMDI5IEwgMTQ5IDIwMTUgTCAxNjcgMjAwNSBMIDE4NiAxOTk0IEwgMjEwIDE5NzkgTCAyMzEgMTk2NSBMIDI2NiAxOTQxIEwgMjg0IDE5MjggTCAyOTUgMTkyMCBMIDMwNiAxOTExIEwgMzI0IDE4OTggTCAzMzggMTg4NyBMIDM0OSAxODc3IEwgMzU4IDE4NzAgTCAzNjkgMTg2MCBMIDM4MCAxODUxIEwgMzgwIDE4NDkgTCAzODIgMTg0OSBMIDM4NSAxODQ2IEwgMzk2IDE4MzcgTCA0MDMgMTgzMCBMIDQxMSAxODIzIEwgNDE4IDE4MTYgTCA0MjYgMTgwOSBMIDQ3NSAxNzYwIEwgNDc3IDE3NTYgTCA0NzkgMTc1NiBMIDQ4NyAxNzQ3IEwgNTAzIDE3MzAgTCA1MTAgMTcyMiBMIDUxOSAxNzEyIEwgNTI2IDE3MDQgTCA1MzggMTY5MSBMIDU0NyAxNjgwIEwgNTU4IDE2NjcgTCA1NjYgMTY1NiBMIDU3OSAxNjQwIEwgNTkwIDE2MjYgTCA2MDYgMTYwNSBMIDYyMSAxNTgzIEwgNjMxIDE1NjkgTCA2NDUgMTU0OSBMIDY1NSAxNTMzIEwgNjcwIDE1MTAgTCA2ODUgMTQ4NSBMIDY5NiAxNDY3IEwgNzA2IDE0NDkgTCA3MzAgMTQwNSBMIDc0NSAxMzc0IEwgNzYyIDEzMzggTCA3NzQgMTMxMCBMIDc5MiAxMjY3IEwgODExIDEyMTYgTCA4MjcgMTE2NiBMIDg0MiAxMTE3IEwgODU4IDEwNTEgTCA4NzAgOTk2IEwgODc1IDk2NyBMIDg4MSA5MTcgTCA4ODYgODgyIEwgODkxIDg0NiBMIDg5NCA4MTIgTCA4OTYgNzc0IEwgODk2IDY1NSBMIDg5NSA2MzAgTCA4OTIgNTkzIEwgODg4IDU1OSBMIDg3NyA0NzAgTCA4NzIgNDQ1IEwgODY1IDQyNiBMIDg1NyA0MTEgTCA4NDcgMzk1IEwgODM0IDM3OCBMIDgxNCAzNTggTCA4MDAgMzQ3IEwgNzg1IDMzNyBMIDc2NiAzMjcgTCA3NTAgMzIxIEwgNzI3IDMxNSBMIDY5MyAzMDggTCA2NTAgMzAxIEwgNTk3IDI5MiBMIDU3MCAyODYgTCA1NDggMjgwIEwgNTE1IDI3MCBMIDQ4NyAyNjEgTCA0NTkgMjUwIEwgNDM4IDI0MSBMIDQxNCAyMzAgTCAzODIgMjEzIEwgMzU4IDE5OSBMIDM0NyAxOTIgTCAzMzAgMTgwIEwgMzEyIDE2NyBMIDMwMCAxNTcgTCAyODYgMTQ2IEwgMjcxIDEzMyBMIDI2MyAxMjYgTCAyNTEgMTE1IEwgMjQwIDEwNiBMIDIyOCA5NSBMIDIwMCA3MSBMIDE4OSA2MiBMIDE3NCA1MCBMIDE1OCAzOCBMIDE0MiAyNyBMIDEyNyAxOSBMIDExNSAxNCBMIDk1IDggTCA3MSAwIEwgMCAwIHogTSAzMCA2MjIgTCA1MSA2MjMgTCA3OSA2MjcgTCAxMDAgNjMzIEwgMTIxIDY0MSBMIDE0MSA2NTIgTCAxNTcgNjYzIEwgMTcyIDY3NiBMIDE4NiA2OTAgTCAxODYgNjkyIEwgMTg4IDY5MiBMIDIwNiA3MTYgTCAyMTYgNzM0IEwgMjIxIDc0NSBMIDIyNiA3NTggTCAyMzMgNzg0IEwgMjM2IDc5OCBMIDIzNyA4MDggTCAyMzcgODQwIEwgMjMzIDg2NSBMIDIyNiA4OTEgTCAyMTkgOTA5IEwgMjA5IDkyOCBMIDE5NSA5NDkgTCAxODYgOTU5IEwgMTc5IDk2NyBMIDE2NiA5ODAgTCAxNTUgOTg5IEwgMTQyIDk5OCBMIDEyNiAxMDA3IEwgMTEzIDEwMTIgTCAxMTQgMTAyNCBMIDEyMiAxMDYwIEwgMTM3IDExMjMgTCAxNTYgMTIwOCBMIDE3MCAxMjcxIEwgMTc0IDEyOTYgTCAxNzYgMTMxMCBMIDE3NiAxMzM1IEwgMTczIDEzNDkgTCAxNjYgMTM1OSBMIDE1NyAxMzY3IEwgMTQ2IDEzNzIgTCAxNDAgMTM3NCBMIDExOSAxMzc2IEwgNTEgMTM3NiBMIC0zMiAxMzc3IEwgLTY0IDEzNzcgTCAtNzggMTM3NCBMIC04NSAxMzcwIEwgLTk4IDEzNTkgTCAtMTA2IDEzNDkgTCAtMTEwIDEzNDEgTCAtMTEyIDEzMzMgTCAtMTEyIDEzMTggTCAtMTA3IDEyODggTCAtMTAwIDEyNTYgTCAtODUgMTE5NiBMIC02OSAxMTI0IEwgLTU3IDEwNzIgTCAtNTAgMTAzNiBMIC00NyAxMDExIEwgLTUzIDEwMDkgTCAtNjkgMTAwMSBMIC04MSA5OTQgTCAtOTMgOTg1IEwgLTEwMyA5NzYgTCAtMTExIDk2OSBMIC0xMjAgOTYwIEwgLTEzMSA5NDYgTCAtMTQyIDkyOSBMIC0xNTIgOTEwIEwgLTE2MCA4OTAgTCAtMTY2IDg3MCBMIC0xNjkgODUzIEwgLTE3MCA4NDQgTCAtMTcwIDgxNCBMIC0xNjcgNzk0IEwgLTE2MSA3NjcgTCAtMTU0IDc0NiBMIC0xMzcgNzEzIEwgLTEyNiA2OTggTCAtMTE5IDY5MCBMIC0xMDggNjc4IEwgLTkzIDY2NSBMIC03NyA2NTMgTCAtNTYgNjQxIEwgLTM4IDYzMyBMIC0xNiA2MjcgTCAzIDYyNCBMIDMwIDYyMiB6ICIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkxKSIgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==",
|
||||
"icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyMDQ4IDIwNDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5OTEpIiBkPSJtMCAwaDcxbDI0IDggMjAgNiAxMiA1IDE1IDggMTYgMTEgMTYgMTIgMTUgMTIgMTEgOSAyOCAyNCAxMiAxMSAxMSA5IDEyIDExIDggNyAxNSAxMyAxNCAxMSAxMiAxMCAxOCAxMyAxNyAxMiAxMSA3IDI0IDE0IDMyIDE3IDI0IDExIDIxIDkgMjggMTEgMjggOSAzMyAxMCAyMiA2IDI3IDYgNTMgOSA0MyA3IDM0IDcgMjMgNiAxNiA2IDE5IDEwIDE1IDEwIDE0IDExIDIwIDIwIDEzIDE3IDEwIDE2IDggMTUgNyAxOSA1IDI1IDExIDg5IDQgMzQgMyAzNyAxIDI1djExOWwtMiAzOC0zIDM0LTUgMzYtNSAzNS02IDUwLTUgMjktMTIgNTUtMTYgNjYtMTUgNDktMTYgNTAtMTkgNTEtMTggNDMtMTIgMjgtMTcgMzYtMTUgMzEtMjQgNDQtMTAgMTgtMTEgMTgtMTUgMjUtMTUgMjMtMTAgMTYtMTQgMjAtMTAgMTQtMTUgMjItMTYgMjEtMTEgMTQtMTMgMTYtOCAxMS0xMSAxMy05IDExLTEyIDEzLTcgOC05IDEwLTcgOC0xNiAxNy04IDloLTJsLTIgNC00OSA0OS04IDctNyA3LTggNy03IDctMTEgOS0zIDNoLTJ2MmwtMTEgOS0xMSAxMC05IDctMTEgMTAtMTQgMTEtMTggMTMtMTEgOS0xMSA4LTE4IDEzLTM1IDI0LTIxIDE0LTI0IDE1LTE5IDExLTE4IDEwLTI4IDE0LTIxIDgtMjcgNy0xMiAzLTEgMWgtNDhsLTQ0LTExLTIwLTctMjAtOS0xNi04LTI0LTEzLTIxLTEzLTE1LTEwLTIwLTE0LTE2LTExLTEyLTktMTktMTQtMTMtMTAtMTYtMTItMTctMTQtMTAtOC0xNy0xNC0yNi0yMi0xMy0xMi04LTctMTYtMTUtMTctMTYtNTctNTctNy04LTEwLTEwLTctOC0xMS0xMi0xMC0xMS05LTExLTI4LTM0LTEzLTE2LTEzLTE3LTExLTE1LTE0LTE5LTEzLTE4LTEzLTE5LTEwLTE1LTEzLTIwLTEzLTIxLTE5LTMyLTE0LTI1LTE1LTI4LTIxLTQyLTE1LTMzLTE2LTM4LTE0LTM2LTEwLTI5LTctMTgtMTQtNDUtMTgtNjQtNi0yNi0xMS01Mi0xMi02My00LTI5LTYtNjItNi03Mi0zLTUyLTEtMzB2LTU5bDItNjEgMy00NyA1LTQ1IDYtMzUgNi0xOCA5LTE5IDEwLTE4IDEwLTE1IDgtMTAgNS02aDJsMi00IDgtOCAxMS05IDEwLTcgMjEtMTMgMTYtOCAxNy02IDM0LTggMzItNyA2NC0xMSA0OS05IDMwLTggMjMtOCA0Mi0xNCAyMS05IDMzLTE1IDIzLTExIDIxLTEyIDE4LTEwIDIxLTE0IDIwLTEzIDExLTggMTMtMTAgMTQtMTEgMTEtOSAxMy0xMSA4LTcgOS05aDJ2LTJsOC03IDEwLTEwaDJsMS0zIDgtNyAxMy0xMyAxMS05IDctNyAxOS0xNCAxNC05IDE5LTExIDE5LTcgMjMtNyAxMC0zeiIgZmlsbD0iI0YxODQyOSIvPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDIxLDYyMikiIGQ9Im0wIDAgMjEgMSAyOCA0IDIxIDYgMjEgOCAyMCAxMSAxNiAxMSAxNSAxMyAxNCAxNHYyaDJsMTggMjQgMTAgMTggNSAxMSA1IDEzIDcgMjYgMyAxNCAxIDEwdjMybC00IDI1LTcgMjYtNyAxOC0xMCAxOS0xNCAyMS05IDEwLTcgOC0xMyAxMy0xMSA5LTEzIDktMTYgOS0xMyA1IDEgMTIgOCAzNiAxNSA2MyAxOSA4NSAxNCA2MyA0IDI1IDIgMTR2MjVsLTMgMTQtNyAxMC05IDgtMTEgNS02IDItMjEgMmgtNjhsLTgzIDFoLTMybC0xNC0zLTctNC0xMy0xMS04LTEwLTQtOC0yLTh2LTE1bDUtMzAgNy0zMiAxNS02MCAxNi03MiAxMi01MiA3LTM2IDMtMjUtNi0yLTE2LTgtMTItNy0xMi05LTEwLTktOC03LTktOS0xMS0xNC0xMS0xNy0xMC0xOS04LTIwLTYtMjAtMy0xNy0xLTl2LTMwbDMtMjAgNi0yNyA3LTIxIDE3LTMzIDExLTE1IDctOCAxMS0xMiAxNS0xMyAxNi0xMiAyMS0xMiAxOC04IDIyLTYgMTktM3oiIGZpbGw9IiNGRUZGRkUiLz4KPC9zdmc+Cg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"a10c6dd9-465e-4226-8198-c7c44b91c555": {
|
||||
"name": "Kaspersky Password Manager",
|
||||
"icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzc0ODRfODc0NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMV83NDg0Xzg3NDUpIj48ZyBjbGlwLXBhdGg9InVybCgjY2xpcDJfNzQ4NF84NzQ1KSI+PHBhdGggZD0iTTI4MS45NjQgNi45NjE3MUMyNjUuODkzIC0yLjI5OTc0IDI0Ni4xMDcgLTIuMjk5NzQgMjMwLjAzNiA2Ljk2MTcxTDQ2LjAzNjUgMTEzLjAwMkMyOS45MjcxIDEyMi4yODYgMjAgMTM5LjQ2NiAyMCAxNTguMDZWMzUzLjk3MkMyMCAzNzIuNTY2IDI5LjkyNzEgMzg5Ljc0NSA0Ni4wMzY1IDM5OS4wMjlMMjMwLjAzNiA1MDUuMDdDMjQ2LjEwNyA1MTQuMzMxIDI2NS44OTMgNTE0LjMzMSAyODEuOTY0IDUwNS4wN0w0NjUuOTY0IDM5OS4wMjlDNDgyLjA3MyAzODkuNzQ1IDQ5MiAzNzIuNTY2IDQ5MiAzNTMuOTcyVjE1OC4wNkM0OTIgMTM5LjQ2NiA0ODIuMDczIDEyMi4yODYgNDY1Ljk2NCAxMTMuMDAyTDI4MS45NjQgNi45NjE3MVoiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl83NDg0Xzg3NDUpIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00NTMuOTggMTMzLjc5OEwyNjkuOTggMjcuNzU3NEMyNjEuMzI3IDIyLjc3MDQgMjUwLjY3MyAyMi43NzA0IDI0Mi4wMiAyNy43NTc0TDU4LjAxOTYgMTMzLjc5OEM0OS4zNDUzIDEzOC43OTcgNDQgMTQ4LjA0NyA0NCAxNTguMDZWMzUzLjk3MkM0NCAzNjMuOTg0IDQ5LjM0NTMgMzczLjIzNCA1OC4wMTk2IDM3OC4yMzNMMjQyLjAyIDQ4NC4yNzRDMjUwLjY3MyA0ODkuMjYxIDI2MS4zMjcgNDg5LjI2MSAyNjkuOTggNDg0LjI3NEw0NTMuOTggMzc4LjIzM0M0NjIuNjU1IDM3My4yMzQgNDY4IDM2My45ODQgNDY4IDM1My45NzJWMTU4LjA2QzQ2OCAxNDguMDQ3IDQ2Mi42NTUgMTM4Ljc5NyA0NTMuOTggMTMzLjc5OFpNMjgxLjk2NCA2Ljk2MTcxQzI2NS44OTMgLTIuMjk5NzQgMjQ2LjEwNyAtMi4yOTk3NCAyMzAuMDM2IDYuOTYxNzFMNDYuMDM2NSAxMTMuMDAyQzI5LjkyNzEgMTIyLjI4NiAyMCAxMzkuNDY2IDIwIDE1OC4wNlYzNTMuOTcyQzIwIDM3Mi41NjYgMjkuOTI3MSAzODkuNzQ1IDQ2LjAzNjUgMzk5LjAyOUwyMzAuMDM2IDUwNS4wN0MyNDYuMTA3IDUxNC4zMzEgMjY1Ljg5MyA1MTQuMzMxIDI4MS45NjQgNTA1LjA3TDQ2NS45NjQgMzk5LjAyOUM0ODIuMDczIDM4OS43NDUgNDkyIDM3Mi41NjYgNDkyIDM1My45NzJWMTU4LjA2QzQ5MiAxMzkuNDY2IDQ4Mi4wNzMgMTIyLjI4NiA0NjUuOTY0IDExMy4wMDJMMjgxLjk2NCA2Ljk2MTcxWiIgZmlsbD0iYmxhY2siLz48L2c+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yNTYgMTQwQzI0Mi43NDUgMTQwIDIzMiAxNTAuNzQ1IDIzMiAxNjRDMjMyIDE3Ny4yNTUgMjQyLjc0NSAxODggMjU2IDE4OEMyNjkuMjU1IDE4OCAyODAgMTc3LjI1NSAyODAgMTY0QzI4MCAxNTAuNzQ1IDI2OS4yNTUgMTQwIDI1NiAxNDBaTTI0OCAxNjRDMjQ4IDE1OS41ODIgMjUxLjU4MiAxNTYgMjU2IDE1NkMyNjAuNDE4IDE1NiAyNjQgMTU5LjU4MiAyNjQgMTY0QzI2NCAxNjguNDE4IDI2MC40MTggMTcyIDI1NiAxNzJDMjUxLjU4MiAxNzIgMjQ4IDE2OC40MTggMjQ4IDE2NFoiIGZpbGw9ImJsYWNrIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNzIgMTkyQzE3MiAxNDUuNjA4IDIwOS42MDggMTA4IDI1NiAxMDhDMzAyLjM5MiAxMDggMzQwIDE0NS42MDggMzQwIDE5MkMzNDAgMjI3LjA2MSAzMTguNTE5IDI1Ny4xMDUgMjg4IDI2OS42OVYzODYuNjdDMjg4IDM5Mi4zOTEgMjg0Ljk0NiAzOTcuNjc2IDI3OS45ODkgNDAwLjUzM0wyNjMuOTg5IDQwOS43NTNDMjU5LjA0NCA0MTIuNjAzIDI1Mi45NTYgNDEyLjYwMyAyNDguMDExIDQwOS43NTNMMjMyLjAxMSA0MDAuNTMzQzIyNy4wNTQgMzk3LjY3NiAyMjQgMzkyLjM5MSAyMjQgMzg2LjY3VjM3MkMyMjQgMzY5Ljg3OCAyMjQuODQzIDM2Ny44NDQgMjI2LjM0MyAzNjYuMzQzTDIzNiAzNTYuNjg2VjM1NS4zMTRMMjI2LjM0MyAzNDUuNjU3QzIyNC44NDMgMzQ0LjE1NyAyMjQgMzQyLjEyMiAyMjQgMzQwVjMzMkMyMjQgMzI5Ljg3OCAyMjQuODQzIDMyNy44NDQgMjI2LjM0MyAzMjYuMzQzTDIzNiAzMTYuNjg2VjMxNS4zMTRMMjI2LjM0MyAzMDUuNjU3QzIyNC44NDMgMzA0LjE1NyAyMjQgMzAyLjEyMiAyMjQgMzAwVjI2OS42OUMxOTMuNDgxIDI1Ny4xMDUgMTcyIDIyNy4wNjEgMTcyIDE5MlpNMjU2IDEyNEMyMTguNDQ1IDEyNCAxODggMTU0LjQ0NSAxODggMTkyQzE4OCAyMjkuNTU1IDIxOC40NDUgMjYwIDI1NiAyNjBDMjkzLjU1NSAyNjAgMzI0IDIyOS41NTUgMzI0IDE5MkMzMjQgMTU0LjQ0NSAyOTMuNTU1IDEyNCAyNTYgMTI0Wk0yNTYgMjc2QzI2MS40NzEgMjc2IDI2Ni44MiAyNzUuNDc3IDI3MiAyNzQuNDc4VjM4Ni42N0wyNTYgMzk1Ljg5TDI0MCAzODYuNjdWMzc1LjMxNEwyNDkuNjU3IDM2NS42NTdDMjUxLjE1NyAzNjQuMTU3IDI1MiAzNjIuMTIyIDI1MiAzNjBWMzUyQzI1MiAzNDkuODc4IDI1MS4xNTcgMzQ3Ljg0NCAyNDkuNjU3IDM0Ni4zNDNMMjQwIDMzNi42ODZWMzM1LjMxNEwyNDkuNjU3IDMyNS42NTdDMjUxLjE1NyAzMjQuMTU3IDI1MiAzMjIuMTIyIDI1MiAzMjBWMzEyQzI1MiAzMDkuODc4IDI1MS4xNTcgMzA3Ljg0NCAyNDkuNjU3IDMwNi4zNDNMMjQwIDI5Ni42ODZWMjc0LjQ3OEMyNDUuMTggMjc1LjQ3NyAyNTAuNTI5IDI3NiAyNTYgMjc2WiIgZmlsbD0iYmxhY2siLz48L2c+PC9nPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl83NDg0Xzg3NDUiIHgxPSIzOTMuODY1IiB5MT0iNjMuMjc5NiIgeDI9Ijk5LjIwNDMiIHkyPSI0MjkuOTk4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzRERkY4OCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzNERThDQSIvPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJjbGlwMF83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwMV83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwMl83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==",
|
||||
"icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzc0ODRfODc0NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMV83NDg0Xzg3NDUpIj48ZyBjbGlwLXBhdGg9InVybCgjY2xpcDJfNzQ4NF84NzQ1KSI+PHBhdGggZD0iTTI4MS45NjQgNi45NjE3MUMyNjUuODkzIC0yLjI5OTc0IDI0Ni4xMDcgLTIuMjk5NzQgMjMwLjAzNiA2Ljk2MTcxTDQ2LjAzNjUgMTEzLjAwMkMyOS45MjcxIDEyMi4yODYgMjAgMTM5LjQ2NiAyMCAxNTguMDZWMzUzLjk3MkMyMCAzNzIuNTY2IDI5LjkyNzEgMzg5Ljc0NSA0Ni4wMzY1IDM5OS4wMjlMMjMwLjAzNiA1MDUuMDdDMjQ2LjEwNyA1MTQuMzMxIDI2NS44OTMgNTE0LjMzMSAyODEuOTY0IDUwNS4wN0w0NjUuOTY0IDM5OS4wMjlDNDgyLjA3MyAzODkuNzQ1IDQ5MiAzNzIuNTY2IDQ5MiAzNTMuOTcyVjE1OC4wNkM0OTIgMTM5LjQ2NiA0ODIuMDczIDEyMi4yODYgNDY1Ljk2NCAxMTMuMDAyTDI4MS45NjQgNi45NjE3MVoiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl83NDg0Xzg3NDUpIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00NTMuOTggMTMzLjc5OEwyNjkuOTggMjcuNzU3NEMyNjEuMzI3IDIyLjc3MDQgMjUwLjY3MyAyMi43NzA0IDI0Mi4wMiAyNy43NTc0TDU4LjAxOTYgMTMzLjc5OEM0OS4zNDUzIDEzOC43OTcgNDQgMTQ4LjA0NyA0NCAxNTguMDZWMzUzLjk3MkM0NCAzNjMuOTg0IDQ5LjM0NTMgMzczLjIzNCA1OC4wMTk2IDM3OC4yMzNMMjQyLjAyIDQ4NC4yNzRDMjUwLjY3MyA0ODkuMjYxIDI2MS4zMjcgNDg5LjI2MSAyNjkuOTggNDg0LjI3NEw0NTMuOTggMzc4LjIzM0M0NjIuNjU1IDM3My4yMzQgNDY4IDM2My45ODQgNDY4IDM1My45NzJWMTU4LjA2QzQ2OCAxNDguMDQ3IDQ2Mi42NTUgMTM4Ljc5NyA0NTMuOTggMTMzLjc5OFpNMjgxLjk2NCA2Ljk2MTcxQzI2NS44OTMgLTIuMjk5NzQgMjQ2LjEwNyAtMi4yOTk3NCAyMzAuMDM2IDYuOTYxNzFMNDYuMDM2NSAxMTMuMDAyQzI5LjkyNzEgMTIyLjI4NiAyMCAxMzkuNDY2IDIwIDE1OC4wNlYzNTMuOTcyQzIwIDM3Mi41NjYgMjkuOTI3MSAzODkuNzQ1IDQ2LjAzNjUgMzk5LjAyOUwyMzAuMDM2IDUwNS4wN0MyNDYuMTA3IDUxNC4zMzEgMjY1Ljg5MyA1MTQuMzMxIDI4MS45NjQgNTA1LjA3TDQ2NS45NjQgMzk5LjAyOUM0ODIuMDczIDM4OS43NDUgNDkyIDM3Mi41NjYgNDkyIDM1My45NzJWMTU4LjA2QzQ5MiAxMzkuNDY2IDQ4Mi4wNzMgMTIyLjI4NiA0NjUuOTY0IDExMy4wMDJMMjgxLjk2NCA2Ljk2MTcxWiIgZmlsbD0iYmxhY2siLz48L2c+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yNTYgMTQwQzI0Mi43NDUgMTQwIDIzMiAxNTAuNzQ1IDIzMiAxNjRDMjMyIDE3Ny4yNTUgMjQyLjc0NSAxODggMjU2IDE4OEMyNjkuMjU1IDE4OCAyODAgMTc3LjI1NSAyODAgMTY0QzI4MCAxNTAuNzQ1IDI2OS4yNTUgMTQwIDI1NiAxNDBaTTI0OCAxNjRDMjQ4IDE1OS41ODIgMjUxLjU4MiAxNTYgMjU2IDE1NkMyNjAuNDE4IDE1NiAyNjQgMTU5LjU4MiAyNjQgMTY0QzI2NCAxNjguNDE4IDI2MC40MTggMTcyIDI1NiAxNzJDMjUxLjU4MiAxNzIgMjQ4IDE2OC40MTggMjQ4IDE2NFoiIGZpbGw9ImJsYWNrIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNzIgMTkyQzE3MiAxNDUuNjA4IDIwOS42MDggMTA4IDI1NiAxMDhDMzAyLjM5MiAxMDggMzQwIDE0NS42MDggMzQwIDE5MkMzNDAgMjI3LjA2MSAzMTguNTE5IDI1Ny4xMDUgMjg4IDI2OS42OVYzODYuNjdDMjg4IDM5Mi4zOTEgMjg0Ljk0NiAzOTcuNjc2IDI3OS45ODkgNDAwLjUzM0wyNjMuOTg5IDQwOS43NTNDMjU5LjA0NCA0MTIuNjAzIDI1Mi45NTYgNDEyLjYwMyAyNDguMDExIDQwOS43NTNMMjMyLjAxMSA0MDAuNTMzQzIyNy4wNTQgMzk3LjY3NiAyMjQgMzkyLjM5MSAyMjQgMzg2LjY3VjM3MkMyMjQgMzY5Ljg3OCAyMjQuODQzIDM2Ny44NDQgMjI2LjM0MyAzNjYuMzQzTDIzNiAzNTYuNjg2VjM1NS4zMTRMMjI2LjM0MyAzNDUuNjU3QzIyNC44NDMgMzQ0LjE1NyAyMjQgMzQyLjEyMiAyMjQgMzQwVjMzMkMyMjQgMzI5Ljg3OCAyMjQuODQzIDMyNy44NDQgMjI2LjM0MyAzMjYuMzQzTDIzNiAzMTYuNjg2VjMxNS4zMTRMMjI2LjM0MyAzMDUuNjU3QzIyNC44NDMgMzA0LjE1NyAyMjQgMzAyLjEyMiAyMjQgMzAwVjI2OS42OUMxOTMuNDgxIDI1Ny4xMDUgMTcyIDIyNy4wNjEgMTcyIDE5MlpNMjU2IDEyNEMyMTguNDQ1IDEyNCAxODggMTU0LjQ0NSAxODggMTkyQzE4OCAyMjkuNTU1IDIxOC40NDUgMjYwIDI1NiAyNjBDMjkzLjU1NSAyNjAgMzI0IDIyOS41NTUgMzI0IDE5MkMzMjQgMTU0LjQ0NSAyOTMuNTU1IDEyNCAyNTYgMTI0Wk0yNTYgMjc2QzI2MS40NzEgMjc2IDI2Ni44MiAyNzUuNDc3IDI3MiAyNzQuNDc4VjM4Ni42N0wyNTYgMzk1Ljg5TDI0MCAzODYuNjdWMzc1LjMxNEwyNDkuNjU3IDM2NS42NTdDMjUxLjE1NyAzNjQuMTU3IDI1MiAzNjIuMTIyIDI1MiAzNjBWMzUyQzI1MiAzNDkuODc4IDI1MS4xNTcgMzQ3Ljg0NCAyNDkuNjU3IDM0Ni4zNDNMMjQwIDMzNi42ODZWMzM1LjMxNEwyNDkuNjU3IDMyNS42NTdDMjUxLjE1NyAzMjQuMTU3IDI1MiAzMjIuMTIyIDI1MiAzMjBWMzEyQzI1MiAzMDkuODc4IDI1MS4xNTcgMzA3Ljg0NCAyNDkuNjU3IDMwNi4zNDNMMjQwIDI5Ni42ODZWMjc0LjQ3OEMyNDUuMTggMjc1LjQ3NyAyNTAuNTI5IDI3NiAyNTYgMjc2WiIgZmlsbD0iYmxhY2siLz48L2c+PC9nPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl83NDg0Xzg3NDUiIHgxPSIzOTMuODY1IiB5MT0iNjMuMjc5NiIgeDI9Ijk5LjIwNDMiIHkyPSI0MjkuOTk4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzRERkY4OCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzNERThDQSIvPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJjbGlwMF83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwMV83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwMl83NDg0Xzg3NDUiPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg=="
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-27 04:32
|
||||
|
||||
import authentik.lib.utils.time
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def convert_integer_to_string_format(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
|
||||
for stage in EmailStage.objects.using(db_alias).all():
|
||||
stage.token_expiry = f"minutes={stage.token_expiry}"
|
||||
stage.save(using=db_alias)
|
||||
|
||||
|
||||
def convert_string_to_integer_format(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
|
||||
for stage in EmailStage.objects.using(db_alias).all():
|
||||
# Check if token_expiry is a string
|
||||
if isinstance(stage.token_expiry, str):
|
||||
try:
|
||||
# Use the timedelta_from_string utility to convert to timedelta
|
||||
# then convert to minutes by dividing seconds by 60
|
||||
td = timedelta_from_string(stage.token_expiry)
|
||||
minutes_value = int(td.total_seconds() / 60)
|
||||
stage.token_expiry = minutes_value
|
||||
stage.save(using=db_alias)
|
||||
except (ValueError, TypeError):
|
||||
# If the string can't be parsed or converted properly, skip
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_email", "0004_emailstage_activate_user_on_success"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="emailstage",
|
||||
name="token_expiry",
|
||||
field=models.TextField(
|
||||
default="minutes=30",
|
||||
help_text="Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
convert_integer_to_string_format,
|
||||
convert_string_to_integer_format,
|
||||
),
|
||||
]
|
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -74,8 +75,10 @@ class EmailStage(Stage):
|
||||
default=False, help_text=_("Activate users upon completion of stage.")
|
||||
)
|
||||
|
||||
token_expiry = models.IntegerField(
|
||||
default=30, help_text=_("Time in minutes the token sent is valid.")
|
||||
token_expiry = models.TextField(
|
||||
default="minutes=30",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_("Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."),
|
||||
)
|
||||
subject = models.TextField(default="authentik")
|
||||
template = models.TextField(default=EmailTemplates.PASSWORD_RESET)
|
||||
|
@ -22,6 +22,7 @@ from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDI
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -73,8 +74,8 @@ class EmailStageView(ChallengeStageView):
|
||||
"""Get token"""
|
||||
pending_user = self.get_pending_user()
|
||||
current_stage: EmailStage = self.executor.current_stage
|
||||
valid_delta = timedelta(
|
||||
minutes=current_stage.token_expiry + 1
|
||||
valid_delta = timedelta_from_string(current_stage.token_expiry) + timedelta(
|
||||
minutes=1
|
||||
) # + 1 because django timesince always rounds down
|
||||
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
|
||||
# Don't check for validity here, we only care if the token exists
|
||||
|
@ -5,7 +5,7 @@ entries:
|
||||
- attrs:
|
||||
designation: stage_configuration
|
||||
name: default-authenticator-totp-setup
|
||||
title: Setup Two-Factor authentication
|
||||
title: Set up Two-Factor authentication
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-authenticator-totp-setup
|
||||
|
@ -57,7 +57,7 @@ entries:
|
||||
use_ssl: false
|
||||
timeout: 10
|
||||
from_address: system@authentik.local
|
||||
token_expiry: 30
|
||||
token_expiry: minutes=30
|
||||
subject: authentik
|
||||
template: email/password_reset.html
|
||||
activate_user_on_success: true
|
||||
|
@ -6661,6 +6661,16 @@
|
||||
"title": "Token",
|
||||
"description": "Authentication token"
|
||||
},
|
||||
"compatibility_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"aws",
|
||||
"slack"
|
||||
],
|
||||
"title": "SCIM Compatibility Mode",
|
||||
"description": "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
},
|
||||
"exclude_users_service_account": {
|
||||
"type": "boolean",
|
||||
"title": "Exclude users service account"
|
||||
@ -6669,6 +6679,11 @@
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Filter group"
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"title": "Dry run",
|
||||
"description": "When enabled, provider will not modify or create objects in the remote system."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@ -11364,11 +11379,10 @@
|
||||
"title": "From address"
|
||||
},
|
||||
"token_expiry": {
|
||||
"type": "integer",
|
||||
"minimum": -2147483648,
|
||||
"maximum": 2147483647,
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Token expiry",
|
||||
"description": "Time in minutes the token sent is valid."
|
||||
"description": "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
@ -14196,6 +14210,11 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Default group email domain"
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"title": "Dry run",
|
||||
"description": "When enabled, provider will not modify or create objects in the remote system."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@ -14344,6 +14363,11 @@
|
||||
"suspend"
|
||||
],
|
||||
"title": "Group delete action"
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"title": "Dry run",
|
||||
"description": "When enabled, provider will not modify or create objects in the remote system."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
16
go.mod
16
go.mod
@ -6,7 +6,7 @@ toolchain go1.24.0
|
||||
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/coreos/go-oidc/v3 v3.13.0
|
||||
github.com/getsentry/sentry-go v0.31.1
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
@ -22,17 +22,17 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/redis/go-redis/v9 v9.7.1
|
||||
github.com/sethvargo/go-envconfig v1.1.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025020.1
|
||||
goauthentik.io/api/v3 v3.2025021.4
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
@ -76,9 +76,9 @@ require (
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
34
go.sum
34
go.sum
@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -239,8 +239,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025020.1 h1:7922W4XiGif7lUCl2qlaeQJ3wSx1wDDDpXx8ryx0Hv0=
|
||||
goauthentik.io/api/v3 v3.2025020.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025021.4 h1:KFap2KW+8CwhOxjBkRnRB4flvuHEMw24+fZei9dOhzw=
|
||||
goauthentik.io/api/v3 v3.2025021.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -313,8 +313,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -386,16 +386,17 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -410,8 +411,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -449,8 +450,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -471,8 +472,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1001.0",
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1001.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1001.0.tgz",
|
||||
"integrity": "sha512-Wp6fKNXcxBm+f8U1GkLV4gEgqq1pu5uwyDCMBg7ZB/6CtP+PsD/mPhuKyMULNWucDvYN8oy70XLOkMnxa3NWFw==",
|
||||
"version": "2.1004.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1004.0.tgz",
|
||||
"integrity": "sha512-3E5ICmSc7ZCZCwLX7NY+HFmmdUYgRaL+67h/BDoDQmkhx9StC8wG4xgzHFY9k8WQS0+ib/MP28f2d9yzHtQLlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1001.0",
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,9 @@ def wait_for_db():
|
||||
# Sanity check, ensure SECRET_KEY is set before we even check for database connectivity
|
||||
if CONFIG.get("secret_key") is None or len(CONFIG.get("secret_key")) == 0:
|
||||
CONFIG.log("info", "----------------------------------------------------------------------")
|
||||
CONFIG.log("info", "Secret key missing, check https://goauthentik.io/docs/installation/.")
|
||||
CONFIG.log(
|
||||
"info", "Secret key missing, check https://docs.goauthentik.io/docs/install-config/"
|
||||
)
|
||||
CONFIG.log("info", "----------------------------------------------------------------------")
|
||||
sysexit(1)
|
||||
check_postgres()
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -865,6 +865,12 @@ msgstr ""
|
||||
msgid "Invalid next URL"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr ""
|
||||
@ -879,6 +885,10 @@ msgstr ""
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
@ -1365,6 +1375,14 @@ msgstr ""
|
||||
msgid "ES256 (Asymmetric Encryption)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
msgstr ""
|
||||
@ -1865,6 +1883,18 @@ msgstr ""
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr ""
|
||||
@ -1873,6 +1903,14 @@ msgstr ""
|
||||
msgid "Authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr ""
|
||||
@ -2517,6 +2555,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr ""
|
||||
|
||||
@ -2855,10 +2894,6 @@ msgstr ""
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr ""
|
||||
|
Binary file not shown.
@ -9,9 +9,9 @@
|
||||
# Kyllian Delaye-Maillot, 2023
|
||||
# Manuel Viens, 2023
|
||||
# Mordecai, 2023
|
||||
# Charles Leclerc, 2024
|
||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||
# Tina, 2024
|
||||
# Charles Leclerc, 2025
|
||||
# Marc Schmitt, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -948,6 +948,14 @@ msgstr "Jetons du flux"
|
||||
msgid "Invalid next URL"
|
||||
msgstr "URL suivante invalide"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr ""
|
||||
"Si activé, le fournisseur ne changera ou ne créera pas d'objets auprès du "
|
||||
"système distant."
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr "Démarrage d'une synchronisation complète du fournisseur"
|
||||
@ -962,6 +970,10 @@ msgstr "Synchronisation de la page {page} d'utilisateurs"
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Synchronisation de la page {page} de groupes"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr "Abandon de la requête de mutation en raison d'une simulation"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
@ -1510,6 +1522,14 @@ msgstr "RS256 (chiffrement asymétrique)"
|
||||
msgid "ES256 (Asymmetric Encryption)"
|
||||
msgstr "ES256 (Chiffrement Asymétrique)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr "ES384 (chiffrement asymétrique)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr "ES512 (chiffrement asymétrique)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
msgstr "Portées utilisées par le client"
|
||||
@ -2077,6 +2097,18 @@ msgstr "Fournisseur SAML depuis métadonnées"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "Fournisseurs SAML depuis métadonnées"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "Par défaut"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2"
|
||||
@ -2085,6 +2117,16 @@ msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2
|
||||
msgid "Authentication token"
|
||||
msgstr "Jeton d'authentification"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "Mode de compatibilité SCIM"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
"Change le comportement d'authentik en fonction des spécificités "
|
||||
"d'implémentations des fournisseurs SCIM."
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "Fournisseur SCIM"
|
||||
@ -2777,6 +2819,7 @@ msgstr ""
|
||||
"les paramètres de connexion ci-dessous seront ignorés."
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr ""
|
||||
"Durée de validité du jeton envoyé (Format : hours=3,minutes=17,seconds=300)."
|
||||
@ -3148,10 +3191,6 @@ msgstr "Confirmation du Compte"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "Activer les utilisateurs à la complétion de l'étape."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "Temps en minutes durant lequel le jeton envoyé est valide."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "Étape Email"
|
||||
|
Binary file not shown.
Binary file not shown.
@ -7,7 +7,7 @@
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Tianhao Chai <cth451@gmail.com>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2025
|
||||
# deluxghost, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -878,6 +878,12 @@ msgstr "流程令牌"
|
||||
msgid "Invalid next URL"
|
||||
msgstr "无效的 next URL"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr "启用时,提供程序将不会在远程系统上修改或创建对象。"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr "开始全量提供程序同步"
|
||||
@ -892,6 +898,10 @@ msgstr "正在同步用户页面 {page}"
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "正在同步群组页面 {page}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr "由于启用了试运行,已放弃变更请求"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
@ -1389,6 +1399,14 @@ msgstr "RS256(非对称加密)"
|
||||
msgid "ES256 (Asymmetric Encryption)"
|
||||
msgstr "ES256(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr "ES384(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr "ES512(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
msgstr "客户端使用的作用域"
|
||||
@ -1891,6 +1909,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "来自元数据的 SAML 提供程序"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
@ -1899,6 +1929,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
msgid "Authentication token"
|
||||
msgstr "身份验证令牌"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "SCIM 兼容模式"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "SCIM 提供程序"
|
||||
@ -2553,6 +2591,7 @@ msgid ""
|
||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr "发出令牌有效的时间(格式:hours=3,minutes=17,seconds=300)。"
|
||||
|
||||
@ -2902,10 +2941,6 @@ msgstr "账户确认"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "完成阶段后激活用户。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "电子邮件阶段"
|
||||
|
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
# Translators:
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2025
|
||||
# deluxghost, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-25 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -877,6 +877,12 @@ msgstr "流程令牌"
|
||||
msgid "Invalid next URL"
|
||||
msgstr "无效的 next URL"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr "启用时,提供程序将不会在远程系统上修改或创建对象。"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr "开始全量提供程序同步"
|
||||
@ -891,6 +897,10 @@ msgstr "正在同步用户页面 {page}"
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "正在同步群组页面 {page}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr "由于启用了试运行,已放弃变更请求"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
@ -1388,6 +1398,14 @@ msgstr "RS256(非对称加密)"
|
||||
msgid "ES256 (Asymmetric Encryption)"
|
||||
msgstr "ES256(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr "ES384(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr "ES512(非对称加密)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
msgstr "客户端使用的作用域"
|
||||
@ -1890,6 +1908,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "来自元数据的 SAML 提供程序"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
@ -1898,6 +1928,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
msgid "Authentication token"
|
||||
msgstr "身份验证令牌"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "SCIM 兼容模式"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "SCIM 提供程序"
|
||||
@ -2552,6 +2590,7 @@ msgid ""
|
||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr "发出令牌有效的时间(格式:hours=3,minutes=17,seconds=300)。"
|
||||
|
||||
@ -2901,10 +2940,6 @@ msgstr "账户确认"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "完成阶段后激活用户。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "电子邮件阶段"
|
||||
|
666
poetry.lock
generated
666
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,32 @@ version = "2025.2.1"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["**/node_modules/**"]
|
||||
|
||||
[tool.codespell]
|
||||
skip = [
|
||||
"**/node_modules",
|
||||
"**/package-lock.json",
|
||||
"schema.yml",
|
||||
"unittest.xml",
|
||||
"./blueprints/schema.json",
|
||||
"go.sum",
|
||||
"locale",
|
||||
"**/dist",
|
||||
"**/storybook-static",
|
||||
"**/web/src/locales",
|
||||
"**/web/xliff",
|
||||
"./web/storybook-static",
|
||||
"./website/build",
|
||||
"./gen-ts-api",
|
||||
"./gen-py-api",
|
||||
"./gen-go-api",
|
||||
"*.api.mdx",
|
||||
"./htmlcov",
|
||||
]
|
||||
dictionary = ".github/codespell-dictionary.txt,-"
|
||||
ignore-words = ".github/codespell-words.txt"
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py312']
|
||||
@ -123,7 +149,9 @@ kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
msgraph-sdk = "*"
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = [
|
||||
"reggie",
|
||||
] }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
|
82
schema.yml
82
schema.yml
@ -35146,7 +35146,7 @@ paths:
|
||||
- in: query
|
||||
name: token_expiry
|
||||
schema:
|
||||
type: integer
|
||||
type: string
|
||||
- in: query
|
||||
name: use_global_settings
|
||||
schema:
|
||||
@ -41582,6 +41582,12 @@ components:
|
||||
- confidential
|
||||
- public
|
||||
type: string
|
||||
CompatibilityModeEnum:
|
||||
enum:
|
||||
- default
|
||||
- aws
|
||||
- slack
|
||||
type: string
|
||||
Config:
|
||||
type: object
|
||||
description: Serialize authentik Config into DRF Object
|
||||
@ -42774,10 +42780,8 @@ components:
|
||||
format: email
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
template:
|
||||
@ -42833,10 +42837,9 @@ components:
|
||||
minLength: 1
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -44154,6 +44157,10 @@ components:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
default_group_email_domain:
|
||||
type: string
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
@ -44317,6 +44324,10 @@ components:
|
||||
default_group_email_domain:
|
||||
type: string
|
||||
minLength: 1
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- credentials
|
||||
- default_group_email_domain
|
||||
@ -46397,6 +46408,10 @@ components:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
group_delete_action:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
@ -46557,6 +46572,10 @@ components:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
group_delete_action:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
@ -50373,10 +50392,9 @@ components:
|
||||
minLength: 1
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -50679,6 +50697,10 @@ components:
|
||||
default_group_email_domain:
|
||||
type: string
|
||||
minLength: 1
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
PatchedGroupKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
@ -51260,6 +51282,10 @@ components:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
group_delete_action:
|
||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
PatchedNotificationRequest:
|
||||
type: object
|
||||
description: Notification Serializer
|
||||
@ -52421,12 +52447,21 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
PatchedSCIMSourceGroupRequest:
|
||||
type: object
|
||||
description: SCIMSourceGroup Serializer
|
||||
@ -55817,12 +55852,21 @@ components:
|
||||
token:
|
||||
type: string
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
@ -55903,12 +55947,21 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
the remote system.
|
||||
required:
|
||||
- name
|
||||
- token
|
||||
@ -57105,6 +57158,9 @@ components:
|
||||
sync_object_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
override_dry_run:
|
||||
type: boolean
|
||||
default: false
|
||||
required:
|
||||
- sync_object_id
|
||||
- sync_object_model
|
||||
|
1
scripts/generate_config.py
Normal file → Executable file
1
scripts/generate_config.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate config for development"""
|
||||
|
||||
from yaml import safe_dump
|
||||
|
15
scripts/generate_semver.py
Executable file
15
scripts/generate_semver.py
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generates a Semantic Versioning identifier, suffixed with a timestamp.
|
||||
"""
|
||||
|
||||
from time import time
|
||||
|
||||
from authentik import __version__ as package_version
|
||||
|
||||
"""
|
||||
See: https://semver.org/#spec-item-9 (Pre-release spec)
|
||||
"""
|
||||
pre_release_timestamp = int(time())
|
||||
|
||||
print(f"{package_version}-{pre_release_timestamp}")
|
@ -1,7 +0,0 @@
|
||||
"""Helper script to generate an NPM Version"""
|
||||
|
||||
from time import time
|
||||
|
||||
from authentik import __version__
|
||||
|
||||
print(f"{__version__}-{int(time())}")
|
141
web/build-observer-plugin.mjs
Normal file
141
web/build-observer-plugin.mjs
Normal file
@ -0,0 +1,141 @@
|
||||
import * as http from "http";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Serializes a custom event to a text stream.
|
||||
* a
|
||||
* @param {Event} event
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeCustomEventToStream(event) {
|
||||
// @ts-ignore
|
||||
const data = event.detail ?? {};
|
||||
|
||||
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
|
||||
|
||||
return eventContent.join("\n") + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the build observer plugin.
|
||||
*
|
||||
* @typedef {Object} BuildObserverOptions
|
||||
*
|
||||
* @property {URL} serverURL
|
||||
* @property {string} logPrefix
|
||||
* @property {string} relativeRoot
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
|
||||
*
|
||||
* @param {BuildObserverOptions} options
|
||||
* @returns {import('esbuild').Plugin}
|
||||
*/
|
||||
export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
|
||||
const timerLabel = `[${logPrefix}] Build`;
|
||||
const endpoint = serverURL.pathname;
|
||||
const dispatcher = new EventTarget();
|
||||
|
||||
const eventServer = http.createServer((req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (req.url !== endpoint) {
|
||||
console.log(`🚫 Invalid request to ${req.url}`);
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔌 Client connected");
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
const listener = (event) => {
|
||||
const body = serializeCustomEventToStream(event);
|
||||
|
||||
res.write(body);
|
||||
};
|
||||
|
||||
dispatcher.addEventListener("esbuild:start", listener);
|
||||
dispatcher.addEventListener("esbuild:error", listener);
|
||||
dispatcher.addEventListener("esbuild:end", listener);
|
||||
|
||||
req.on("close", () => {
|
||||
console.log("🔌 Client disconnected");
|
||||
|
||||
clearInterval(keepAliveInterval);
|
||||
|
||||
dispatcher.removeEventListener("esbuild:start", listener);
|
||||
dispatcher.removeEventListener("esbuild:error", listener);
|
||||
dispatcher.removeEventListener("esbuild:end", listener);
|
||||
});
|
||||
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
console.timeStamp("🏓 Keep-alive");
|
||||
|
||||
res.write("event: keep-alive\n\n");
|
||||
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
return {
|
||||
name: "build-watcher",
|
||||
setup: (build) => {
|
||||
eventServer.listen(parseInt(serverURL.port, 10), serverURL.hostname);
|
||||
|
||||
build.onDispose(() => {
|
||||
eventServer.close();
|
||||
});
|
||||
|
||||
build.onStart(() => {
|
||||
console.time(timerLabel);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:start", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
build.onEnd((buildResult) => {
|
||||
console.timeEnd(timerLabel);
|
||||
|
||||
if (!buildResult.errors.length) {
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:end", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Build ended with ${buildResult.errors.length} errors`);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:error", {
|
||||
detail: buildResult.errors.map((error) => ({
|
||||
...error,
|
||||
location: error.location
|
||||
? {
|
||||
...error.location,
|
||||
file: path.resolve(relativeRoot, error.location.file),
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
289
web/build.mjs
289
web/build.mjs
@ -1,45 +1,54 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import * as chokidar from "chokidar";
|
||||
import esbuild from "esbuild";
|
||||
import fs from "fs";
|
||||
import findFreePorts from "find-free-ports";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
||||
import { globSync } from "glob";
|
||||
import path from "path";
|
||||
import { cwd } from "process";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
import { buildObserverPlugin } from "./build-observer-plugin.mjs";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
let authentikProjectRoot = __dirname + "../";
|
||||
|
||||
try {
|
||||
// Use the package.json file in the root folder, as it has the current version information.
|
||||
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf8",
|
||||
}).replace("\n", "");
|
||||
} catch (_exc) {
|
||||
// We probably don't have a .git folder, which could happen in container builds
|
||||
} catch (_error) {
|
||||
// We probably don't have a .git folder, which could happen in container builds.
|
||||
}
|
||||
const rootPackage = JSON.parse(fs.readFileSync(path.join(authentikProjectRoot, "./package.json")));
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const packageJSONPath = path.join(authentikProjectRoot, "./package.json");
|
||||
const rootPackage = JSON.parse(readFileSync(packageJSONPath, "utf8"));
|
||||
|
||||
const apiBasePath = process.env.AK_API_BASE_PATH || "";
|
||||
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||
const AK_API_BASE_PATH = process.env.AK_API_BASE_PATH || "";
|
||||
|
||||
const envGitHashKey = "GIT_BUILD_HASH";
|
||||
const environmentVars = new Map([
|
||||
["NODE_ENV", NODE_ENV],
|
||||
["CWD", cwd()],
|
||||
["AK_API_BASE_PATH", AK_API_BASE_PATH],
|
||||
]);
|
||||
|
||||
const definitions = {
|
||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
|
||||
};
|
||||
const definitions = Object.fromEntries(
|
||||
Array.from(environmentVars).map(([key, value]) => {
|
||||
return [`process.env.${key}`, JSON.stringify(value)];
|
||||
}),
|
||||
);
|
||||
|
||||
// All is magic is just to make sure the assets are copied into the right places. This is a very
|
||||
// stripped down version of what the rollup-copy-plugin does, without any of the features we don't
|
||||
// use, and using globSync instead of globby since we already had globSync lying around thanks to
|
||||
// Typescript. If there's a third argument in an array entry, it's used to replace the internal path
|
||||
// before concatenating it all together as the destination target.
|
||||
|
||||
const otherFiles = [
|
||||
/**
|
||||
* All is magic is just to make sure the assets are copied into the right places. This is a very
|
||||
* stripped down version of what the rollup-copy-plugin does, without any of the features we don't
|
||||
* use, and using globSync instead of globby since we already had globSync lying around thanks to
|
||||
* Typescript. If there's a third argument in an array entry, it's used to replace the internal path
|
||||
* before concatenating it all together as the destination target.
|
||||
* @type {Array<[string, string, string?]>}
|
||||
*/
|
||||
const assetsFileMappings = [
|
||||
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
||||
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
||||
["src/custom.css", "."],
|
||||
@ -48,28 +57,47 @@ const otherFiles = [
|
||||
["./icons/*", "./assets/icons"],
|
||||
];
|
||||
|
||||
const isFile = (filePath) => fs.statSync(filePath).isFile();
|
||||
/**
|
||||
* @param {string} filePath
|
||||
*/
|
||||
const isFile = (filePath) => statSync(filePath).isFile();
|
||||
|
||||
/**
|
||||
* @param {string} src Source file
|
||||
* @param {string} dest Destination folder
|
||||
* @param {string} [strip] Path to strip from the source file
|
||||
*/
|
||||
function nameCopyTarget(src, dest, strip) {
|
||||
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
|
||||
return [src, target];
|
||||
}
|
||||
|
||||
for (const [source, rawdest, strip] of otherFiles) {
|
||||
for (const [source, rawdest, strip] of assetsFileMappings) {
|
||||
const matchedPaths = globSync(source);
|
||||
const dest = path.join("dist", rawdest);
|
||||
|
||||
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
|
||||
|
||||
for (const [src, dest] of copyTargets) {
|
||||
if (isFile(src)) {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
mkdirSync(path.dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This starts the definitions used for esbuild: Our targets, our arguments, the function for
|
||||
// running a build, and three options for building: watching, building, and building the proxy.
|
||||
// Ordered by largest to smallest interface to build even faster
|
||||
const interfaces = [
|
||||
/**
|
||||
* @typedef {[source: string, destination: string]} EntryPoint
|
||||
*/
|
||||
|
||||
/**
|
||||
* This starts the definitions used for esbuild: Our targets, our arguments, the function for
|
||||
* running a build, and three options for building: watching, building, and building the proxy.
|
||||
* Ordered by largest to smallest interface to build even faster
|
||||
*
|
||||
* @type {EntryPoint[]}
|
||||
*/
|
||||
const entryPoints = [
|
||||
["admin/AdminInterface/AdminInterface.ts", "admin"],
|
||||
["user/UserInterface.ts", "user"],
|
||||
["flow/FlowInterface.ts", "flow"],
|
||||
@ -79,11 +107,14 @@ const interfaces = [
|
||||
["polyfill/poly.ts", "."],
|
||||
];
|
||||
|
||||
const baseArgs = {
|
||||
/**
|
||||
* @satisfies {import("esbuild").BuildOptions}
|
||||
*/
|
||||
const BASE_ESBUILD_OPTIONS = {
|
||||
bundle: true,
|
||||
write: true,
|
||||
sourcemap: true,
|
||||
minify: isProdBuild,
|
||||
minify: NODE_ENV === "production",
|
||||
splitting: true,
|
||||
treeShaking: true,
|
||||
external: ["*.woff", "*.woff2"],
|
||||
@ -95,6 +126,7 @@ const baseArgs = {
|
||||
},
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
plugins: [],
|
||||
logOverride: {
|
||||
/**
|
||||
* HACK: Silences issue originating in ESBuild.
|
||||
@ -106,91 +138,144 @@ const baseArgs = {
|
||||
},
|
||||
};
|
||||
|
||||
function getVersion() {
|
||||
let version = rootPackage.version;
|
||||
if (process.env[envGitHashKey]) {
|
||||
version = `${version}+${process.env[envGitHashKey]}`;
|
||||
/**
|
||||
* Creates a version ID for the build.
|
||||
* @returns {string}
|
||||
*/
|
||||
function composeVersionID() {
|
||||
const { version } = rootPackage;
|
||||
const buildHash = process.env.GIT_BUILD_HASH;
|
||||
|
||||
if (buildHash) {
|
||||
return `${version}+${buildHash}`;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
async function buildOneSource(source, dest) {
|
||||
const DIST = path.join(__dirname, "./dist", dest);
|
||||
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
|
||||
/**
|
||||
* Build a single entry point.
|
||||
*
|
||||
* @param {EntryPoint} buildTarget
|
||||
* @param {Partial<esbuild.BuildOptions>} [overrides]
|
||||
* @throws {Error} on build failure
|
||||
*/
|
||||
function createEntryPointOptions([source, dest], overrides = {}) {
|
||||
const outdir = path.join(__dirname, "./dist", dest);
|
||||
|
||||
try {
|
||||
const start = Date.now();
|
||||
await esbuild.build({
|
||||
...baseArgs,
|
||||
entryPoints: [`./src/${source}`],
|
||||
entryNames: `[dir]/[name]-${getVersion()}`,
|
||||
outdir: DIST,
|
||||
});
|
||||
const end = Date.now();
|
||||
console.log(
|
||||
`[${new Date(end).toISOString()}] Finished build for target ${source} in ${
|
||||
Date.now() - start
|
||||
}ms`,
|
||||
);
|
||||
return 0;
|
||||
} catch (exc) {
|
||||
console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`);
|
||||
return 1;
|
||||
}
|
||||
return {
|
||||
...BASE_ESBUILD_OPTIONS,
|
||||
entryPoints: [`./src/${source}`],
|
||||
entryNames: `[dir]/[name]-${composeVersionID()}`,
|
||||
outdir,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAuthentik(interfaces) {
|
||||
const code = await Promise.allSettled(
|
||||
interfaces.map(([source, dest]) => buildOneSource(source, dest)),
|
||||
/**
|
||||
* Build all entry points in parallel.
|
||||
*
|
||||
* @param {EntryPoint[]} entryPoints
|
||||
*/
|
||||
async function buildParallel(entryPoints) {
|
||||
await Promise.allSettled(
|
||||
entryPoints.map((entryPoint) => {
|
||||
return esbuild.build(createEntryPointOptions(entryPoint));
|
||||
}),
|
||||
);
|
||||
const finalCode = code.reduce((a, res) => a + res.value, 0);
|
||||
if (finalCode > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
let timeoutId = null;
|
||||
function debouncedBuild() {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
console.clear();
|
||||
buildAuthentik(interfaces);
|
||||
}, 250);
|
||||
}
|
||||
function doHelp() {
|
||||
console.log(`Build the authentik UI
|
||||
|
||||
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
|
||||
console.log(`Build the authentikUI
|
||||
|
||||
options:
|
||||
-w, --watch: Build all ${interfaces.length} interfaces
|
||||
-p, --proxy: Build only the polyfills and the loading application
|
||||
-h, --help: This help message
|
||||
options:
|
||||
-w, --watch: Build all ${entryPoints.length} interfaces
|
||||
-p, --proxy: Build only the polyfills and the loading application
|
||||
-h, --help: This help message
|
||||
`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) {
|
||||
console.log("Watching ./src for changes");
|
||||
chokidar.watch("./src").on("all", (event, path) => {
|
||||
if (!["add", "change", "unlink"].includes(event)) {
|
||||
return;
|
||||
}
|
||||
if (!/(\.css|\.ts|\.js)$/.test(path)) {
|
||||
return;
|
||||
}
|
||||
debouncedBuild();
|
||||
});
|
||||
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
||||
// There's no watch-for-proxy, sorry.
|
||||
process.exit(
|
||||
await buildAuthentik(
|
||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||
),
|
||||
async function doWatch() {
|
||||
console.log("Watching all entry points...");
|
||||
|
||||
const wathcherPorts = await findFreePorts(entryPoints.length);
|
||||
|
||||
const buildContexts = await Promise.all(
|
||||
entryPoints.map((entryPoint, i) => {
|
||||
const port = wathcherPorts[i];
|
||||
const serverURL = new URL(`http://localhost:${port}/events`);
|
||||
|
||||
return esbuild.context(
|
||||
createEntryPointOptions(entryPoint, {
|
||||
plugins: [
|
||||
...BASE_ESBUILD_OPTIONS.plugins,
|
||||
buildObserverPlugin({
|
||||
serverURL,
|
||||
logPrefix: entryPoint[1],
|
||||
relativeRoot: __dirname,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
...definitions,
|
||||
"process.env.WATCHER_URL": JSON.stringify(serverURL.toString()),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(buildContexts.map((context) => context.rebuild()));
|
||||
|
||||
await Promise.allSettled(buildContexts.map((context) => context.watch()));
|
||||
|
||||
return /** @type {Promise<void>} */ (
|
||||
new Promise((resolve) => {
|
||||
process.on("SIGINT", () => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// And the fallback: just build it.
|
||||
process.exit(await buildAuthentik(interfaces));
|
||||
}
|
||||
|
||||
async function doBuild() {
|
||||
console.log("Building all entry points");
|
||||
|
||||
return buildParallel(entryPoints);
|
||||
}
|
||||
|
||||
async function doProxy() {
|
||||
return buildParallel(
|
||||
entryPoints.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||
);
|
||||
}
|
||||
|
||||
async function delegateCommand() {
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case "-h":
|
||||
case "--help":
|
||||
return doHelp();
|
||||
case "-w":
|
||||
case "--watch":
|
||||
return doWatch();
|
||||
// There's no watch-for-proxy, sorry.
|
||||
case "-p":
|
||||
case "--proxy":
|
||||
return doProxy();
|
||||
default:
|
||||
return doBuild();
|
||||
}
|
||||
}
|
||||
|
||||
await delegateCommand()
|
||||
.then(() => {
|
||||
console.log("Build complete");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
55
web/package-lock.json
generated
55
web/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2025.2.1-1740653734",
|
||||
"@goauthentik/api": "^2025.2.1-1741798605",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -37,11 +37,12 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.4",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
@ -89,6 +90,7 @@
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
"eslint-plugin-wc": "^2.1.1",
|
||||
"find-free-ports": "^3.1.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"globals": "^15.10.0",
|
||||
@ -824,9 +826,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime-corejs3": {
|
||||
"version": "7.25.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.25.7.tgz",
|
||||
"integrity": "sha512-gMmIEhg35sXk9Te5qbGp3W9YKrvLt3HV658/d3odWrHSqT0JeG5OzsJWFHRLiOohRyjRsJc/x03DhJm3i8VJxg==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.10.tgz",
|
||||
"integrity": "sha512-uITFQYO68pMEYR46AHgQoyBg7KPPJDAbGn4jUTIRgCFJIp88MIBUianVOplhZDEec07bp9zIyr4Kp0FCyQzmWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js-pure": "^3.30.2",
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@ -1814,9 +1817,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2025.2.1-1740653734",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.1-1740653734.tgz",
|
||||
"integrity": "sha512-GRxBt52lgZOvEu7l9DN1lj0L2Q9KUiftrC9MWfaz3dIlw1s+kKzic/NTTlB7AaEsRqw7+i10aI6GkiKAErw2VA=="
|
||||
"version": "2025.2.1-1741798605",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.1-1741798605.tgz",
|
||||
"integrity": "sha512-Go0Iij1q/imohOSqxj43pvju8D+OFH7iNBBg6FO1ytd9pcHi1QY7/jq1vy1HKWZw7oZ2fY6hZnSe+keRnBA0Fg=="
|
||||
},
|
||||
"node_modules/@goauthentik/web": {
|
||||
"resolved": "",
|
||||
@ -9531,13 +9534,13 @@
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-adapter-moment": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz",
|
||||
"integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==",
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=3.0.0",
|
||||
"moment": "^2.10.2"
|
||||
"chart.js": ">=2.8.0",
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
@ -10769,6 +10772,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
@ -12892,6 +12904,13 @@
|
||||
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-free-ports": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/find-free-ports/-/find-free-ports-3.1.1.tgz",
|
||||
"integrity": "sha512-hQebewth9i5qkf0a0u06iFaxQssk5ZnPBBggsa1vk8zCYaZoz9IZXpoRLTbEOrYdqfrjvcxU00gYoCPgmXugKA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@ -16572,6 +16591,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@ -18215,9 +18235,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2025.2.1-1740653734",
|
||||
"@goauthentik/api": "^2025.2.1-1741798605",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -25,11 +25,12 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.4",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
@ -77,6 +78,7 @@
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
"eslint-plugin-wc": "^2.1.1",
|
||||
"find-free-ports": "^3.1.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"globals": "^15.10.0",
|
||||
|
@ -90,12 +90,14 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
|
||||
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
||||
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
||||
updateURLParams({
|
||||
notificationDrawerOpen: this.notificationDrawerOpen,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
||||
this.apiDrawerOpen = !this.apiDrawerOpen;
|
||||
updateURLParams({
|
||||
@ -107,6 +109,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
async firstUpdated(): Promise<void> {
|
||||
configureSentry(true);
|
||||
this.user = await me();
|
||||
|
||||
const canAccessAdmin =
|
||||
this.user.user.isSuperuser ||
|
||||
// TODO: somehow add `access_admin_interface` to the API schema
|
||||
@ -116,6 +119,16 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||
|
||||
new ESBuildObserver(process.env.WATCHER_URL);
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const sidebarClasses = {
|
||||
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
|
||||
|
@ -28,6 +28,7 @@ import { when } from "lit/directives/when.js";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@ -54,6 +55,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
return [
|
||||
PFBase,
|
||||
PFGrid,
|
||||
PFFlex,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFDivider,
|
||||
@ -67,13 +69,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
.card-container {
|
||||
max-height: 10em;
|
||||
}
|
||||
.ak-external-link {
|
||||
display: inline-block;
|
||||
margin-left: 0.175rem;
|
||||
vertical-align: super;
|
||||
line-height: normal;
|
||||
font-size: var(--pf-global--icon--FontSize--sm);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -99,43 +94,34 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
|
||||
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
|
||||
</ak-page-header>
|
||||
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<!-- row 1 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter"
|
||||
>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
||||
<ak-quick-actions-card .actions=${this.quickActions}>
|
||||
</ak-quick-actions-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-zone"
|
||||
header=${msg("Outpost status")}
|
||||
headerLink="#/outpost/outposts"
|
||||
>
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl"
|
||||
>
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
|
||||
<ak-admin-status-chart-sync></ak-admin-status-chart-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col">
|
||||
<hr class="pf-c-divider" />
|
||||
</div>
|
||||
${this.renderCards()}
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl">
|
||||
${this.renderCards()}
|
||||
<div class="pf-l-grid__item pf-m-9-col pf-m-3-row">
|
||||
<ak-recent-events pageSize="6"></ak-recent-events>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col">
|
||||
<hr class="pf-c-divider" />
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-quick-actions-card .actions=${this.quickActions}>
|
||||
</ak-quick-actions-card>
|
||||
</div>
|
||||
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-zone"
|
||||
header=${msg("Outpost status")}
|
||||
headerLink="#/outpost/outposts"
|
||||
>
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
|
||||
<ak-admin-status-chart-sync></ak-admin-status-chart-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
|
||||
<!-- row 3 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container"
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import {
|
||||
APIError,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
} from "@goauthentik/common/errors/network";
|
||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import { ResponseError } from "@goauthentik/api";
|
||||
|
||||
@ -13,46 +18,143 @@ export interface AdminStatus {
|
||||
message?: TemplateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for admin status cards with robust state management
|
||||
*
|
||||
* @template T - Type of the primary data value used in the card
|
||||
*/
|
||||
export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
|
||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||
|
||||
// Current data value state
|
||||
@state()
|
||||
value?: T;
|
||||
|
||||
// Current status state derived from value
|
||||
@state()
|
||||
protected status?: AdminStatus;
|
||||
|
||||
// Current error state if any request fails
|
||||
@state()
|
||||
protected error?: APIError;
|
||||
|
||||
// Abstract methods to be implemented by subclasses
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
// Proper binding for event handler
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
// Register refresh event listener
|
||||
this.addEventListener(EVENT_REFRESH, this.fetchData);
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
// Lifecycle method: Called when component is added to DOM
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Initial data fetch
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch primary data and handle errors
|
||||
*/
|
||||
private fetchData() {
|
||||
this.getPrimaryValue()
|
||||
.then((value: T) => {
|
||||
this.value = value; // Triggers shouldUpdate
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
this.status = undefined;
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit lifecycle method: Determine if component should update
|
||||
*
|
||||
* @param changed - Map of changed properties
|
||||
* @returns boolean indicating if update should proceed
|
||||
*/
|
||||
shouldUpdate(changed: PropertyValues<this>) {
|
||||
if (changed.has("value") && this.value !== undefined) {
|
||||
// When value changes, fetch new status
|
||||
this.getStatus(this.value)
|
||||
.then((status) => {
|
||||
this.status = status;
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch(async (error: ResponseError) => {
|
||||
this.status = undefined;
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
|
||||
// Prevent immediate re-render if only value changed
|
||||
if (changed.size === 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the primary value display
|
||||
*
|
||||
* @returns TemplateResult displaying the value
|
||||
*/
|
||||
protected renderValue(): TemplateResult {
|
||||
return html`${this.value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render status state
|
||||
*
|
||||
* @param status - AdminStatus object containing icon and message
|
||||
* @returns TemplateResult for status display
|
||||
*/
|
||||
private renderStatus(status: AdminStatus): TemplateResult {
|
||||
return html`
|
||||
<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error state
|
||||
*
|
||||
* @param error - Error message to display
|
||||
* @returns TemplateResult for error display
|
||||
*/
|
||||
private renderError(error: string): TemplateResult {
|
||||
return html`
|
||||
<p><i class="fa fa-times"></i> ${msg("Failed to fetch")}</p>
|
||||
<p class="subtext">${error}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render loading state
|
||||
*
|
||||
* @returns TemplateResult for loading spinner
|
||||
*/
|
||||
private renderLoading(): TemplateResult {
|
||||
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render method that selects appropriate state display
|
||||
*
|
||||
* @returns TemplateResult for current component state
|
||||
*/
|
||||
renderInner(): TemplateResult {
|
||||
return html`<p class="center-value">
|
||||
${until(
|
||||
this.getPrimaryValue()
|
||||
.then((v) => {
|
||||
this.value = v;
|
||||
return this.getStatus(v);
|
||||
})
|
||||
.then((status) => {
|
||||
return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||
${status.message
|
||||
? html`<p class="subtext">${status.message}</p>`
|
||||
: html``}`;
|
||||
})
|
||||
.catch((exc: ResponseError) => {
|
||||
return html` <p>
|
||||
<i class="fa fa-times"></i> ${exc.response.statusText}
|
||||
</p>
|
||||
<p class="subtext">${msg("Failed to fetch")}</p>`;
|
||||
}),
|
||||
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
|
||||
)}
|
||||
</p>`;
|
||||
return html`
|
||||
<p class="center-value">
|
||||
${
|
||||
this.status
|
||||
? this.renderStatus(this.status) // Status available
|
||||
: this.error
|
||||
? this.renderError(pluckErrorDetail(this.error)) // Error state
|
||||
: this.renderLoading() // Loading state
|
||||
}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
|
||||
import { EventUser, formatGeoEvent } from "@goauthentik/admin/events/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { actionToLabel } from "@goauthentik/common/labels";
|
||||
@ -10,6 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -38,6 +39,22 @@ export class RecentEventsCard extends Table<Event> {
|
||||
return super.styles.concat(
|
||||
PFCard,
|
||||
css`
|
||||
.pf-c-table__sort.pf-m-selected {
|
||||
background-color: var(--pf-global--BackgroundColor--dark-400);
|
||||
border-block-end: var(--pf-global--BorderWidth--xl) solid var(--ak-accent);
|
||||
|
||||
.pf-c-table__button {
|
||||
--pf-c-table__sort__button__text--Color: var(--ak-accent);
|
||||
color: var(--pf-c-nav__link--m-current--Color);
|
||||
|
||||
.pf-c-table__text {
|
||||
--pf-c-table__sort__button__text--Color: var(
|
||||
--pf-c-nav__link--m-current--Color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-card__title {
|
||||
--pf-c-card__title--FontFamily: var(
|
||||
--pf-global--FontFamily--heading--sans-serif
|
||||
@ -45,7 +62,47 @@ export class RecentEventsCard extends Table<Event> {
|
||||
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
|
||||
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
|
||||
}
|
||||
* {
|
||||
|
||||
td[role="cell"] .ip-address {
|
||||
max-width: 18ch;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
th[role="columnheader"]:nth-child(3) {
|
||||
--pf-c-table--cell--MinWidth: fit-content;
|
||||
--pf-c-table--cell--MaxWidth: none;
|
||||
--pf-c-table--cell--Width: 1%;
|
||||
--pf-c-table--cell--Overflow: visible;
|
||||
--pf-c-table--cell--TextOverflow: clip;
|
||||
--pf-c-table--cell--WhiteSpace: nowrap;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
font-size: var(--pf-global--FontSize--md);
|
||||
font-variant: all-petite-caps;
|
||||
}
|
||||
|
||||
.pf-c-table thead:not(:first-child) {
|
||||
background: hsl(0deg 0% 0% / 10%);
|
||||
|
||||
> tr {
|
||||
border-block-end: 2px solid
|
||||
var(
|
||||
--pf-c-page__header-tools--c-button--m-selected--before--BackgroundColor
|
||||
);
|
||||
font-family: var(--pf-global--FontFamily--heading--sans-serif);
|
||||
}
|
||||
}
|
||||
|
||||
tbody * {
|
||||
word-break: break-all;
|
||||
}
|
||||
`,
|
||||
@ -68,20 +125,57 @@ export class RecentEventsCard extends Table<Event> {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
override groupBy(items: Event[]): [SlottedTemplateResult, Event[]][] {
|
||||
const groupedByDay = new Map<string, Event[]>();
|
||||
|
||||
for (const item of items) {
|
||||
const day = new Date(item.created);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const serializedDay = day.toISOString();
|
||||
|
||||
let dayEvents = groupedByDay.get(serializedDay);
|
||||
if (!dayEvents) {
|
||||
dayEvents = [];
|
||||
groupedByDay.set(serializedDay, dayEvents);
|
||||
}
|
||||
|
||||
dayEvents.push(item);
|
||||
}
|
||||
|
||||
return Array.from(groupedByDay, ([serializedDay, events]) => {
|
||||
const day = new Date(serializedDay);
|
||||
return [
|
||||
html` <div class="pf-c-content group-header">
|
||||
<div>${getRelativeTime(day)}</div>
|
||||
<small>${day.toLocaleDateString()}</small>
|
||||
</div>`,
|
||||
events,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
||||
<small>${item.app}</small>`,
|
||||
<small class="pf-m-monospace">${item.app}</small>`,
|
||||
EventUser(item),
|
||||
html`<div>${getRelativeTime(item.created)}</div>
|
||||
<small>${item.created.toLocaleString()}</small>`,
|
||||
html` <div>${item.clientIp || msg("-")}</div>
|
||||
<small>${EventGeo(item)}</small>`,
|
||||
|
||||
html`<time datetime="${item.created.toISOString()}" class="pf-c-content">
|
||||
<div><small>${item.created.toLocaleTimeString()}</small></div>
|
||||
</time>`,
|
||||
|
||||
html`<div class="ip-address pf-m-monospace">${item.clientIp || msg("-")}</div>
|
||||
<small class="geographic-location">${formatGeoEvent(item)}</small>`,
|
||||
|
||||
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
|
||||
if (this.error) {
|
||||
return super.renderEmpty(inner);
|
||||
}
|
||||
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state header=${msg("No Events found.")}>
|
||||
<div slot="body">${msg("No matching events could be found.")}</div>
|
||||
|
@ -30,11 +30,13 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
const api = new OutpostsApi(DEFAULT_CONFIG);
|
||||
const outposts = await api.outpostsInstancesList({});
|
||||
const outpostStats: SummarizedSyncStatus[] = [];
|
||||
|
||||
await Promise.all(
|
||||
outposts.results.map(async (element) => {
|
||||
const health = await api.outpostsInstancesHealthList({
|
||||
uuid: element.pk || "",
|
||||
});
|
||||
|
||||
const singleStats: SummarizedSyncStatus = {
|
||||
unsynced: 0,
|
||||
healthy: 0,
|
||||
@ -42,9 +44,11 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
total: health.length,
|
||||
label: element.name,
|
||||
};
|
||||
|
||||
if (health.length === 0) {
|
||||
singleStats.unsynced += 1;
|
||||
}
|
||||
|
||||
health.forEach((h) => {
|
||||
if (h.versionOutdated) {
|
||||
singleStats.failed += 1;
|
||||
@ -52,11 +56,14 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
singleStats.healthy += 1;
|
||||
}
|
||||
});
|
||||
|
||||
outpostStats.push(singleStats);
|
||||
}),
|
||||
);
|
||||
|
||||
this.centerText = outposts.pagination.count.toString();
|
||||
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return outpostStats;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
|
||||
import { Application, CoreApi } from "@goauthentik/api";
|
||||
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
|
||||
|
||||
import "./ApplicationWizardHint";
|
||||
|
||||
@ -172,6 +172,29 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html` ${super.renderToolbar()}
|
||||
<ak-forms-confirm
|
||||
successMessage=${msg("Successfully cleared application cache")}
|
||||
errorMessage=${msg("Failed to delete application cache")}
|
||||
action=${msg("Clear cache")}
|
||||
.onConfirm=${() => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesAllCacheClearCreate();
|
||||
}}
|
||||
>
|
||||
<span slot="header"> ${msg("Clear Application cache")} </span>
|
||||
<p slot="body">
|
||||
${msg(
|
||||
"Are you sure you want to clear the application cache? This will cause all policies to be re-evaluated on their next usage.",
|
||||
)}
|
||||
</p>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary" type="button">
|
||||
${msg("Clear cache")}
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-forms-confirm>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
|
||||
import { ValidationError } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
ExtendedValidationError,
|
||||
} from "./types";
|
||||
|
||||
export class ApplicationWizardStep extends WizardStep {
|
||||
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
|
||||
}
|
||||
|
||||
protected removeErrors(
|
||||
keyToDelete: keyof ExtendedValidationError,
|
||||
keyToDelete: keyof ApplicationTransactionValidationError,
|
||||
): ValidationError | undefined {
|
||||
if (!this.wizard.errors) {
|
||||
return undefined;
|
||||
|
@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
get bindingsAsColumns() {
|
||||
return this.wizard.bindings.map((binding, index) => {
|
||||
const { order, enabled, timeout } = binding;
|
||||
const isSet = P.string.minLength(1);
|
||||
const isSet = P.union(P.string.minLength(1), P.number);
|
||||
const policy = match(binding)
|
||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { parseAPIError } from "@goauthentik/common/errors";
|
||||
import { parseAPIResponseError } from "@goauthentik/common/errors/network";
|
||||
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
@ -33,7 +33,7 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
||||
import { ApplicationTransactionValidationError, OneOfProvider } from "../types.js";
|
||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||
|
||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||
@ -131,39 +131,36 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
this.state = "running";
|
||||
|
||||
return (
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.catch(async (resolution: any) => {
|
||||
const errors = (await parseAPIError(
|
||||
await resolution,
|
||||
)) as ExtendedValidationError;
|
||||
.catch(async (resolution) => {
|
||||
const errors =
|
||||
await parseAPIResponseError<ApplicationTransactionValidationError>(resolution);
|
||||
|
||||
// THIS is a really gross special case; if the user is duplicating the name of
|
||||
// an existing provider, the error appears on the `app` (!) error object. We
|
||||
// have to move that to the `provider.name` error field so it shows up in the
|
||||
// right place.
|
||||
if (Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
delete errors.app.provider;
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
// THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object.
|
||||
// We have to move that to the `provider.name` error field so it shows up in the right place.
|
||||
if (Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
|
||||
delete errors.app.provider;
|
||||
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
});
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
@ -232,7 +229,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
||||
const errors = this.wizard.errors;
|
||||
return html` <hr class="pf-c-divider" />
|
||||
${match(errors as ExtendedValidationError)
|
||||
${match(errors as ApplicationTransactionValidationError)
|
||||
.with(
|
||||
{ app: P.nonNullable },
|
||||
() =>
|
||||
@ -328,7 +325,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
if (!(this.wizard && app && provider)) {
|
||||
throw new Error("Submit step received uninitialized wizard context");
|
||||
}
|
||||
// An empty object is truthy, an empty array is falsey. *WAT Javascript*.
|
||||
// An empty object is truthy, an empty array is falsey. *WAT JavaScript*.
|
||||
const keys = Object.keys(this.wizard.errors);
|
||||
return match([this.state, keys])
|
||||
.with(["submitted", P._], () =>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user