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
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
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
|
- name: Setup dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -11,7 +11,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
restart: always
|
restart: always
|
||||||
redis:
|
redis:
|
||||||
image: docker.io/library/redis
|
image: docker.io/library/redis:7
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
restart: always
|
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
|
keypair
|
||||||
keypairs
|
keypairs
|
||||||
hass
|
kubernetes
|
||||||
warmup
|
oidc
|
||||||
ontext
|
ontext
|
||||||
|
openid
|
||||||
|
passwordless
|
||||||
|
plex
|
||||||
|
saml
|
||||||
|
scim
|
||||||
singed
|
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:
|
docusaurus:
|
||||||
patterns:
|
patterns:
|
||||||
- "@docusaurus/*"
|
- "@docusaurus/*"
|
||||||
|
build:
|
||||||
|
patterns:
|
||||||
|
- "@swc/*"
|
||||||
|
- "swc-*"
|
||||||
|
- "lightningcss*"
|
||||||
|
- "@rspack/binding*"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/lifecycle/aws"
|
directory: "/lifecycle/aws"
|
||||||
schedule:
|
schedule:
|
||||||
|
@ -40,7 +40,7 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
uses: ./.github/actions/docker-push-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:
|
matrix:
|
||||||
version:
|
version:
|
||||||
- docs
|
- docs
|
||||||
|
- version-2025-2
|
||||||
- version-2024-12
|
- version-2024-12
|
||||||
- version-2024-10
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Set up QEMU
|
- 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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
|
4
.github/workflows/release-publish.yml
vendored
4
.github/workflows/release-publish.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Set up QEMU
|
- 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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -186,7 +186,7 @@ jobs:
|
|||||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||||
docker cp ${container}:web/ .
|
docker cp ${container}:web/ .
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
@ -32,6 +32,8 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-client-ts
|
||||||
- name: run extract
|
- name: run extract
|
||||||
run: |
|
run: |
|
||||||
poetry run make i18n-extract
|
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.showCountsInTree": true,
|
||||||
"todo-tree.tree.showBadges": true,
|
"todo-tree.tree.showBadges": true,
|
||||||
"yaml.customTags": [
|
"yaml.customTags": [
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
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
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
# Stage 1: Build website
|
# Stage 1: Build website
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
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
|
WORKDIR /work/website
|
||||||
|
|
||||||
|
67
Makefile
67
Makefile
@ -4,34 +4,17 @@
|
|||||||
PWD = $(shell pwd)
|
PWD = $(shell pwd)
|
||||||
UID = $(shell id -u)
|
UID = $(shell id -u)
|
||||||
GID = $(shell id -g)
|
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
|
PY_SOURCES = authentik tests scripts lifecycle .github
|
||||||
GO_SOURCES = cmd internal
|
|
||||||
WEB_SOURCES = web/src web/packages
|
|
||||||
DOCKER_IMAGE ?= "authentik:test"
|
DOCKER_IMAGE ?= "authentik:test"
|
||||||
|
|
||||||
GEN_API_TS = "gen-ts-api"
|
GEN_API_TS = "gen-ts-api"
|
||||||
GEN_API_PY = "gen-py-api"
|
GEN_API_PY = "gen-py-api"
|
||||||
GEN_API_GO = "gen-go-api"
|
GEN_API_GO = "gen-go-api"
|
||||||
|
|
||||||
pg_user := $(shell python -m authentik.lib.config postgresql.user 2>/dev/null)
|
pg_user := $(shell poetry run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||||
pg_host := $(shell python -m authentik.lib.config postgresql.host 2>/dev/null)
|
pg_host := $(shell poetry run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||||
pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
|
pg_name := $(shell poetry run 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
|
|
||||||
|
|
||||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
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 ./...
|
go test -timeout 0 -v -race -cover ./...
|
||||||
|
|
||||||
test: ## Run the server tests and produce a coverage report (locally)
|
test: ## Run the server tests and produce a coverage report (locally)
|
||||||
coverage run manage.py test --keepdb authentik
|
poetry run coverage run manage.py test --keepdb authentik
|
||||||
coverage html
|
poetry run coverage html
|
||||||
coverage report
|
poetry run coverage report
|
||||||
|
|
||||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||||
black $(PY_SOURCES)
|
poetry run black $(PY_SOURCES)
|
||||||
ruff check --fix $(PY_SOURCES)
|
poetry run ruff check --fix $(PY_SOURCES)
|
||||||
|
|
||||||
lint-codespell: ## Reports spelling errors.
|
lint-codespell: ## Reports spelling errors.
|
||||||
codespell -w $(CODESPELL_ARGS)
|
poetry run codespell -w
|
||||||
|
|
||||||
lint: ## Lint the python and golang sources
|
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
|
golangci-lint run -v
|
||||||
|
|
||||||
core-install:
|
core-install:
|
||||||
poetry install
|
poetry install
|
||||||
|
|
||||||
migrate: ## Run the Authentik Django server's migrations
|
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
|
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
|
cd lifecycle/aws && npm run aws-cfn
|
||||||
|
|
||||||
core-i18n-extract:
|
core-i18n-extract:
|
||||||
ak makemessages \
|
poetry run ak makemessages \
|
||||||
--add-location file \
|
--add-location file \
|
||||||
--no-obsolete \
|
--no-obsolete \
|
||||||
--ignore web \
|
--ignore web \
|
||||||
@ -107,11 +90,11 @@ gen-build: ## Extract the schema from the database
|
|||||||
AUTHENTIK_DEBUG=true \
|
AUTHENTIK_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=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_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=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
|
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
|
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 \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--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 \
|
-i /local/schema.yml \
|
||||||
-g python \
|
-g python \
|
||||||
-o /local/${GEN_API_PY} \
|
-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/
|
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
||||||
|
|
||||||
gen-dev-config: ## Generate a local development config file
|
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
|
gen: gen-build gen-client-ts
|
||||||
|
|
||||||
@ -271,21 +254,21 @@ ci--meta-debug:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
black --check $(PY_SOURCES)
|
poetry run black --check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-ruff: ci--meta-debug
|
ci-ruff: ci--meta-debug
|
||||||
ruff check $(PY_SOURCES)
|
poetry run ruff check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-codespell: ci--meta-debug
|
ci-codespell: ci--meta-debug
|
||||||
codespell $(CODESPELL_ARGS) -s
|
poetry run codespell -s
|
||||||
|
|
||||||
ci-bandit: ci--meta-debug
|
ci-bandit: ci--meta-debug
|
||||||
bandit -r $(PY_SOURCES)
|
poetry run bandit -r $(PY_SOURCES)
|
||||||
|
|
||||||
ci-pending-migrations: ci--meta-debug
|
ci-pending-migrations: ci--meta-debug
|
||||||
ak makemigrations --check
|
poetry run ak makemigrations --check
|
||||||
|
|
||||||
ci-test: ci--meta-debug
|
ci-test: ci--meta-debug
|
||||||
coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||||
coverage report
|
poetry run coverage report
|
||||||
coverage xml
|
poetry run coverage xml
|
||||||
|
@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
|
|||||||
|
|
||||||
## Independent audits and pentests
|
## 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
|
## What authentik classifies as a CVE
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from collections.abc import Iterable
|
|||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@ -154,6 +155,17 @@ class SourceViewSet(
|
|||||||
matching_sources.append(source_settings.validated_data)
|
matching_sources.append(source_settings.validated_data)
|
||||||
return Response(matching_sources)
|
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):
|
class UserSourceConnectionSerializer(SourceSerializer):
|
||||||
"""User source connection"""
|
"""User source connection"""
|
||||||
|
@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
|||||||
"name": "authentik Built-in",
|
"name": "authentik Built-in",
|
||||||
"slug": "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):
|
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""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."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
|
|||||||
)
|
)
|
||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
plan.append_stage(in_memory_stage(RedirectToAppStage))
|
||||||
return plan.to_redirect(request, flow)
|
return plan.to_redirect(request, flow)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSeriali
|
|||||||
"user_delete_action",
|
"user_delete_action",
|
||||||
"group_delete_action",
|
"group_delete_action",
|
||||||
"default_group_email_domain",
|
"default_group_email_domain",
|
||||||
|
"dry_run",
|
||||||
]
|
]
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
@ -8,9 +8,10 @@ from httplib2 import HttpLib2Error, HttpLib2ErrorWithResponse
|
|||||||
|
|
||||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
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 (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
BadRequestSyncException,
|
BadRequestSyncException,
|
||||||
|
DryRunRejected,
|
||||||
NotFoundSyncException,
|
NotFoundSyncException,
|
||||||
ObjectExistsSyncException,
|
ObjectExistsSyncException,
|
||||||
StopSync,
|
StopSync,
|
||||||
@ -43,6 +44,8 @@ class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict
|
|||||||
self.domains.append(domain_name)
|
self.domains.append(domain_name)
|
||||||
|
|
||||||
def _request(self, request: HttpRequest):
|
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:
|
try:
|
||||||
response = request.execute()
|
response = request.execute()
|
||||||
except GoogleAuthError as exc:
|
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",
|
"filter_group",
|
||||||
"user_delete_action",
|
"user_delete_action",
|
||||||
"group_delete_action",
|
"group_delete_action",
|
||||||
|
"dry_run",
|
||||||
]
|
]
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from collections.abc import Coroutine
|
|||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
from azure.core.exceptions import (
|
from azure.core.exceptions import (
|
||||||
ClientAuthenticationError,
|
ClientAuthenticationError,
|
||||||
ServiceRequestError,
|
ServiceRequestError,
|
||||||
@ -12,6 +13,7 @@ from azure.identity.aio import ClientSecretCredential
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
from kiota_abstractions.api_error import APIError
|
from kiota_abstractions.api_error import APIError
|
||||||
|
from kiota_abstractions.request_information import RequestInformation
|
||||||
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
||||||
AzureIdentityAuthenticationProvider,
|
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_request_adapter import GraphRequestAdapter, options
|
||||||
from msgraph.graph_service_client import GraphServiceClient
|
from msgraph.graph_service_client import GraphServiceClient
|
||||||
from msgraph_core import GraphClientFactory
|
from msgraph_core import GraphClientFactory
|
||||||
|
from opentelemetry import trace
|
||||||
|
|
||||||
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
||||||
from authentik.events.utils import sanitize_item
|
from authentik.events.utils import sanitize_item
|
||||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
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 (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
BadRequestSyncException,
|
BadRequestSyncException,
|
||||||
|
DryRunRejected,
|
||||||
NotFoundSyncException,
|
NotFoundSyncException,
|
||||||
ObjectExistsSyncException,
|
ObjectExistsSyncException,
|
||||||
StopSync,
|
StopSync,
|
||||||
@ -35,20 +39,24 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_request_adapter(
|
class AuthentikRequestAdapter(GraphRequestAdapter):
|
||||||
credentials: ClientSecretCredential, scopes: list[str] | None = None
|
def __init__(self, auth_provider, provider: MicrosoftEntraProvider, client=None):
|
||||||
) -> GraphRequestAdapter:
|
super().__init__(auth_provider, client)
|
||||||
if scopes:
|
self._provider = provider
|
||||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
|
|
||||||
else:
|
|
||||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
|
|
||||||
|
|
||||||
return GraphRequestAdapter(
|
async def get_http_response_message(
|
||||||
auth_provider=auth_provider,
|
self,
|
||||||
client=GraphClientFactory.create_with_default_middleware(
|
request_info: RequestInformation,
|
||||||
options=options, client=KiotaClientFactory.get_default_client()
|
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](
|
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.credentials = provider.microsoft_credentials()
|
||||||
self.__prefetch_domains()
|
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
|
@property
|
||||||
def client(self):
|
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:
|
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
|
||||||
try:
|
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")
|
@apply_blueprint("system/providers-microsoft-entra.yaml")
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|
||||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||||
# which will cause errors with multiple users
|
# which will cause errors with multiple users
|
||||||
Tenant.objects.update(avatars="none")
|
Tenant.objects.update(avatars="none")
|
||||||
@ -97,6 +96,38 @@ class MicrosoftEntraUserTests(APITestCase):
|
|||||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||||
user_create.assert_called_once()
|
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):
|
def test_user_not_created(self):
|
||||||
"""Test without property mappings, no group is created"""
|
"""Test without property mappings, no group is created"""
|
||||||
self.provider.property_mappings.clear()
|
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
|
This stage uses the override flow token to resume execution of the initial flow the
|
||||||
source stage is bound to."""
|
source stage is bound to."""
|
||||||
|
|
||||||
def dispatch(self):
|
def dispatch(self, *args, **kwargs):
|
||||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
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 = token.plan
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
response = plan.to_redirect(self.request, token.flow)
|
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.core.tests.utils import create_test_flow, create_test_user
|
||||||
from authentik.enterprise.stages.source.models import SourceStage
|
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.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
@ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase):
|
|||||||
self.assertIsNotNone(flow_token)
|
self.assertIsNotNone(flow_token)
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||||
|
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
@ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase):
|
|||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
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.bindings.append(binding)
|
||||||
self.markers.append(marker or StageMarker())
|
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"""
|
"""Insert stage into plan, as immediate next stage"""
|
||||||
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
|
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
|
||||||
self.markers.insert(1, marker or StageMarker())
|
self.markers.insert(index, marker or StageMarker())
|
||||||
|
|
||||||
def redirect(self, destination: str):
|
def redirect(self, destination: str):
|
||||||
"""Insert a redirect stage as next stage"""
|
"""Insert a redirect stage as next stage"""
|
||||||
|
@ -282,16 +282,14 @@ class ConfigLoader:
|
|||||||
|
|
||||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||||
"""Wrapper for get that converts value into int or None if set"""
|
"""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:
|
if value is UNSET:
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except (ValueError, TypeError) as exc:
|
except (ValueError, TypeError) as exc:
|
||||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||||
return default
|
return None
|
||||||
if value is UNSET:
|
|
||||||
return default
|
|
||||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||||
return default
|
return default
|
||||||
|
|
||||||
@ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
|||||||
"sslcert": config.get("postgresql.sslcert"),
|
"sslcert": config.get("postgresql.sslcert"),
|
||||||
"sslkey": config.get("postgresql.sslkey"),
|
"sslkey": config.get("postgresql.sslkey"),
|
||||||
},
|
},
|
||||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
|
||||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
|
||||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
|
||||||
"postgresql.disable_server_side_cursors", False
|
"postgresql.disable_server_side_cursors", False
|
||||||
),
|
),
|
||||||
"TEST": {
|
"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)
|
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)
|
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||||
if config.get_bool("postgresql.use_pgpool", False):
|
if config.get_bool("postgresql.use_pgpool", False):
|
||||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||||
if disable_server_side_cursors is not UNSET:
|
if disable_server_side_cursors is not UNSET:
|
||||||
|
@ -33,6 +33,7 @@ class SyncObjectSerializer(PassiveSerializer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
sync_object_id = CharField()
|
sync_object_id = CharField()
|
||||||
|
override_dry_run = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class SyncObjectResultSerializer(PassiveSerializer):
|
class SyncObjectResultSerializer(PassiveSerializer):
|
||||||
@ -98,6 +99,7 @@ class OutgoingSyncProviderStatusMixin:
|
|||||||
page=1,
|
page=1,
|
||||||
provider_pk=provider.pk,
|
provider_pk=provider.pk,
|
||||||
pk=params.validated_data["sync_object_id"],
|
pk=params.validated_data["sync_object_id"],
|
||||||
|
override_dry_run=params.validated_data["override_dry_run"],
|
||||||
).get()
|
).get()
|
||||||
return Response(SyncObjectResultSerializer(instance={"messages": res}).data)
|
return Response(SyncObjectResultSerializer(instance={"messages": res}).data)
|
||||||
|
|
||||||
|
@ -28,6 +28,14 @@ class Direction(StrEnum):
|
|||||||
remove = "remove"
|
remove = "remove"
|
||||||
|
|
||||||
|
|
||||||
|
SAFE_METHODS = [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"OPTIONS",
|
||||||
|
"TRACE",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BaseOutgoingSyncClient[
|
class BaseOutgoingSyncClient[
|
||||||
TModel: "Model", TConnection: "Model", TSchema: dict, TProvider: "OutgoingSyncProvider"
|
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"""
|
"""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):
|
class StopSync(BaseSyncException):
|
||||||
"""Exception raised when a configuration error should stop the sync process"""
|
"""Exception raised when a configuration error should stop the sync process"""
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
import pglock
|
import pglock
|
||||||
from django.db import connection
|
from django.db import connection, models
|
||||||
from django.db.models import Model, QuerySet, TextChoices
|
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.core.models import Group, User
|
||||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||||
@ -18,6 +19,14 @@ class OutgoingSyncDeleteAction(TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class OutgoingSyncProvider(Model):
|
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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -32,7 +41,7 @@ class OutgoingSyncProvider(Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sync_lock(self) -> pglock.advisory:
|
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(
|
return pglock.advisory(
|
||||||
lock_id=f"goauthentik.io/{connection.schema_name}/providers/outgoing-sync/{str(self.pk)}",
|
lock_id=f"goauthentik.io/{connection.schema_name}/providers/outgoing-sync/{str(self.pk)}",
|
||||||
timeout=0,
|
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.base import Direction
|
||||||
from authentik.lib.sync.outgoing.exceptions import (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
BadRequestSyncException,
|
BadRequestSyncException,
|
||||||
|
DryRunRejected,
|
||||||
StopSync,
|
StopSync,
|
||||||
TransientSyncException,
|
TransientSyncException,
|
||||||
)
|
)
|
||||||
@ -105,7 +106,9 @@ class SyncTasks:
|
|||||||
return
|
return
|
||||||
task.set_status(TaskStatus.SUCCESSFUL, *messages)
|
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)
|
_object_type = path_to_class(object_type)
|
||||||
self.logger = get_logger().bind(
|
self.logger = get_logger().bind(
|
||||||
provider_type=class_to_path(self._provider_model),
|
provider_type=class_to_path(self._provider_model),
|
||||||
@ -116,6 +119,10 @@ class SyncTasks:
|
|||||||
provider = self._provider_model.objects.filter(pk=provider_pk).first()
|
provider = self._provider_model.objects.filter(pk=provider_pk).first()
|
||||||
if not provider:
|
if not provider:
|
||||||
return messages
|
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:
|
try:
|
||||||
client = provider.client_for_model(_object_type)
|
client = provider.client_for_model(_object_type)
|
||||||
except TransientSyncException:
|
except TransientSyncException:
|
||||||
@ -132,6 +139,22 @@ class SyncTasks:
|
|||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
self.logger.debug("skipping object due to SkipObject", obj=obj)
|
self.logger.debug("skipping object due to SkipObject", obj=obj)
|
||||||
continue
|
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:
|
except BadRequestSyncException as exc:
|
||||||
self.logger.warning("failed to sync object", exc=exc, obj=obj)
|
self.logger.warning("failed to sync object", exc=exc, obj=obj)
|
||||||
messages.append(
|
messages.append(
|
||||||
@ -231,8 +254,10 @@ class SyncTasks:
|
|||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
continue
|
continue
|
||||||
|
except DryRunRejected as exc:
|
||||||
|
self.logger.info("Rejected dry-run event", exc=exc)
|
||||||
except StopSync as 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]):
|
def sync_signal_m2m(self, group_pk: str, action: str, pk_set: list[int]):
|
||||||
self.logger = get_logger().bind(
|
self.logger = get_logger().bind(
|
||||||
@ -263,5 +288,7 @@ class SyncTasks:
|
|||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
continue
|
continue
|
||||||
|
except DryRunRejected as exc:
|
||||||
|
self.logger.info("Rejected dry-run event", exc=exc)
|
||||||
except StopSync as 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()
|
test_obj = Test()
|
||||||
dumps(test_obj, indent=4, cls=AttrEncoder)
|
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)
|
@mock.patch.dict(environ, check_deprecations_env_vars)
|
||||||
def test_check_deprecations(self):
|
def test_check_deprecations(self):
|
||||||
"""Test config key re-write for deprecated env vars"""
|
"""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):
|
def test_db_read_replicas(self):
|
||||||
"""Test read replicas"""
|
"""Test read replicas"""
|
||||||
config = ConfigLoader()
|
config = ConfigLoader()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Base Kubernetes Reconciler"""
|
"""Base Kubernetes Reconciler"""
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||||
@ -67,7 +68,8 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Get the name of the object this reconciler manages"""
|
"""Get the name of the object this reconciler manages"""
|
||||||
return (
|
|
||||||
|
base_name = (
|
||||||
self.controller.outpost.config.object_naming_template
|
self.controller.outpost.config.object_naming_template
|
||||||
% {
|
% {
|
||||||
"name": slugify(self.controller.outpost.name),
|
"name": slugify(self.controller.outpost.name),
|
||||||
@ -75,6 +77,16 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
}
|
}
|
||||||
).lower()
|
).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:
|
def get_patched_reference_object(self) -> T:
|
||||||
"""Get patched reference object"""
|
"""Get patched reference object"""
|
||||||
reference = self.get_reference_object()
|
reference = self.get_reference_object()
|
||||||
@ -112,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
try:
|
try:
|
||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
except (OpenApiException, HTTPError) as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
|
|
||||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||||
self.logger.debug("Failed to get current, triggering recreate")
|
self.logger.debug("Failed to get current, triggering recreate")
|
||||||
raise NeedsRecreate from exc
|
raise NeedsRecreate from exc
|
||||||
@ -156,7 +167,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
self.delete(current)
|
self.delete(current)
|
||||||
self.logger.debug("Removing")
|
self.logger.debug("Removing")
|
||||||
except (OpenApiException, HTTPError) as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
|
|
||||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||||
self.logger.debug("Failed to get current, assuming non-existent")
|
self.logger.debug("Failed to get current, assuming non-existent")
|
||||||
return
|
return
|
||||||
|
@ -61,9 +61,14 @@ class KubernetesController(BaseController):
|
|||||||
client: KubernetesClient
|
client: KubernetesClient
|
||||||
connection: KubernetesServiceConnection
|
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)
|
super().__init__(outpost, connection)
|
||||||
self.client = KubernetesClient(connection)
|
self.client = client if client else KubernetesClient(connection)
|
||||||
self.reconcilers = {
|
self.reconcilers = {
|
||||||
SecretReconciler.reconciler_name(): SecretReconciler,
|
SecretReconciler.reconciler_name(): SecretReconciler,
|
||||||
DeploymentReconciler.reconciler_name(): DeploymentReconciler,
|
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 typing import Any
|
||||||
from urllib.parse import urlparse, urlunparse
|
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.rsa import RSAPrivateKey
|
||||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||||
from dacite import Config
|
from dacite import Config
|
||||||
@ -114,6 +119,22 @@ class JWTAlgorithms(models.TextChoices):
|
|||||||
HS256 = "HS256", _("HS256 (Symmetric Encryption)")
|
HS256 = "HS256", _("HS256 (Symmetric Encryption)")
|
||||||
RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
|
RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
|
||||||
ES256 = "ES256", _("ES256 (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):
|
class ScopeMapping(PropertyMapping):
|
||||||
@ -263,11 +284,7 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
|||||||
return self.client_secret, JWTAlgorithms.HS256
|
return self.client_secret, JWTAlgorithms.HS256
|
||||||
key: CertificateKeyPair = self.signing_key
|
key: CertificateKeyPair = self.signing_key
|
||||||
private_key = key.private_key
|
private_key = key.private_key
|
||||||
if isinstance(private_key, RSAPrivateKey):
|
return private_key, JWTAlgorithms.from_private_key(private_key)
|
||||||
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)}")
|
|
||||||
|
|
||||||
def get_issuer(self, request: HttpRequest) -> str | None:
|
def get_issuer(self, request: HttpRequest) -> str | None:
|
||||||
"""Get issuer, based on request"""
|
"""Get issuer, based on request"""
|
||||||
|
@ -254,10 +254,10 @@ class OAuthAuthorizationParams:
|
|||||||
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
||||||
if SCOPE_OFFLINE_ACCESS in self.scope:
|
if SCOPE_OFFLINE_ACCESS in self.scope:
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||||
if PROMPT_CONSENT not in self.prompt:
|
# Don't explicitly request consent with offline_access, as the spec allows for
|
||||||
# Instead of ignoring the `offline_access` scope when `prompt`
|
# "other conditions for processing the request permitting offline access to the
|
||||||
# isn't set to `consent`, we set override it ourselves
|
# requested resources are in place"
|
||||||
self.prompt.add(PROMPT_CONSENT)
|
# which we interpret as "the admin picks an authorization flow with or without consent"
|
||||||
if self.response_type not in [
|
if self.response_type not in [
|
||||||
ResponseTypes.CODE,
|
ResponseTypes.CODE,
|
||||||
ResponseTypes.CODE_TOKEN,
|
ResponseTypes.CODE_TOKEN,
|
||||||
|
@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
|
|||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
LOGGER.warning("Flow not applicable to user")
|
LOGGER.warning("Flow not applicable to user")
|
||||||
return None
|
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)
|
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
|
|||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
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)
|
return plan.to_redirect(self.request, self.flow)
|
||||||
|
@ -75,10 +75,7 @@ class JWKSView(View):
|
|||||||
key_data = {}
|
key_data = {}
|
||||||
|
|
||||||
if use == "sig":
|
if use == "sig":
|
||||||
if isinstance(private_key, RSAPrivateKey):
|
key_data["alg"] = JWTAlgorithms.from_private_key(private_key)
|
||||||
key_data["alg"] = JWTAlgorithms.RS256
|
|
||||||
elif isinstance(private_key, EllipticCurvePrivateKey):
|
|
||||||
key_data["alg"] = JWTAlgorithms.ES256
|
|
||||||
elif use == "enc":
|
elif use == "enc":
|
||||||
key_data["alg"] = "RSA-OAEP-256"
|
key_data["alg"] = "RSA-OAEP-256"
|
||||||
key_data["enc"] = "A256CBC-HS512"
|
key_data["enc"] = "A256CBC-HS512"
|
||||||
|
@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
|||||||
def reconciler_name() -> str:
|
def reconciler_name() -> str:
|
||||||
return "ingress"
|
return "ingress"
|
||||||
|
|
||||||
def _check_annotations(self, reference: V1Ingress):
|
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
|
||||||
"""Check that all annotations *we* set are correct"""
|
"""Check that all annotations *we* set are correct"""
|
||||||
for key, value in self.get_ingress_annotations().items():
|
for key, value in reference.metadata.annotations.items():
|
||||||
if key not in reference.metadata.annotations:
|
if key not in current.metadata.annotations:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
if reference.metadata.annotations[key] != value:
|
if current.metadata.annotations[key] != value:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
|
||||||
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
self._check_annotations(reference)
|
self._check_annotations(current, reference)
|
||||||
# Create a list of all expected host and tls hosts
|
# Create a list of all expected host and tls hosts
|
||||||
expected_hosts = []
|
expected_hosts = []
|
||||||
expected_hosts_tls = []
|
expected_hosts_tls = []
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"""RAC app config"""
|
"""RAC app config"""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from authentik.blueprints.apps import ManagedAppConfig
|
||||||
|
|
||||||
|
|
||||||
class AuthentikProviderRAC(AppConfig):
|
class AuthentikProviderRAC(ManagedAppConfig):
|
||||||
"""authentik rac app config"""
|
"""authentik rac app config"""
|
||||||
|
|
||||||
name = "authentik.providers.rac"
|
name = "authentik.providers.rac"
|
||||||
|
@ -4,8 +4,7 @@ from asgiref.sync import async_to_sync
|
|||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.contrib.auth.signals import user_logged_out
|
from django.contrib.auth.signals import user_logged_out
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||||
from django.db.models.signals import post_save, pre_delete
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Endpoint)
|
@receiver([post_save, post_delete], sender=Endpoint)
|
||||||
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
def post_save_post_delete_endpoint(**_):
|
||||||
"""Clear user's endpoint cache upon endpoint creation"""
|
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
||||||
if not created: # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
# Delete user endpoint cache
|
|
||||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
|
@ -46,7 +46,7 @@ class RACStartView(PolicyAccessView):
|
|||||||
)
|
)
|
||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
plan.insert_stage(
|
plan.append_stage(
|
||||||
in_memory_stage(
|
in_memory_stage(
|
||||||
RACFinalStage,
|
RACFinalStage,
|
||||||
application=self.application,
|
application=self.application,
|
||||||
|
@ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView):
|
|||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
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)
|
return plan.to_redirect(self.request, self.flow)
|
||||||
|
|
||||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
|
@ -28,8 +28,10 @@ class SCIMProviderSerializer(ProviderSerializer):
|
|||||||
"url",
|
"url",
|
||||||
"verify_certificates",
|
"verify_certificates",
|
||||||
"token",
|
"token",
|
||||||
|
"compatibility_mode",
|
||||||
"exclude_users_service_account",
|
"exclude_users_service_account",
|
||||||
"filter_group",
|
"filter_group",
|
||||||
|
"dry_run",
|
||||||
]
|
]
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
@ -12,8 +12,9 @@ from authentik.lib.sync.outgoing import (
|
|||||||
HTTP_SERVICE_UNAVAILABLE,
|
HTTP_SERVICE_UNAVAILABLE,
|
||||||
HTTP_TOO_MANY_REQUESTS,
|
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 (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
|
DryRunRejected,
|
||||||
NotFoundSyncException,
|
NotFoundSyncException,
|
||||||
ObjectExistsSyncException,
|
ObjectExistsSyncException,
|
||||||
TransientSyncException,
|
TransientSyncException,
|
||||||
@ -21,7 +22,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
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:
|
if TYPE_CHECKING:
|
||||||
from django.db.models import Model
|
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:
|
def _request(self, method: str, path: str, **kwargs) -> dict:
|
||||||
"""Wrapper to send a request to the full URL"""
|
"""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:
|
try:
|
||||||
response = self._session.request(
|
response = self._session.request(
|
||||||
method,
|
method,
|
||||||
@ -87,9 +90,14 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
|||||||
"""Get Service provider config"""
|
"""Get Service provider config"""
|
||||||
default_config = ServiceProviderConfiguration.default()
|
default_config = ServiceProviderConfiguration.default()
|
||||||
try:
|
try:
|
||||||
return ServiceProviderConfiguration.model_validate(
|
config = ServiceProviderConfiguration.model_validate(
|
||||||
self._request("GET", "/ServiceProviderConfig")
|
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:
|
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
|
||||||
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
||||||
return default_config
|
return default_config
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"""User client"""
|
"""User client"""
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.http import urlencode
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
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.policies.utils import delete_none_values
|
||||||
from authentik.providers.scim.clients.base import SCIMClient
|
from authentik.providers.scim.clients.base import SCIMClient
|
||||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
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):
|
def create(self, user: User):
|
||||||
"""Create user from scratch and create a connection object"""
|
"""Create user from scratch and create a connection object"""
|
||||||
scim_user = self.to_schema(user, None)
|
scim_user = self.to_schema(user, None)
|
||||||
response = self._request(
|
with transaction.atomic():
|
||||||
"POST",
|
try:
|
||||||
"/Users",
|
response = self._request(
|
||||||
json=scim_user.model_dump(
|
"POST",
|
||||||
mode="json",
|
"/Users",
|
||||||
exclude_unset=True,
|
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`")
|
except ObjectExistsSyncException as exc:
|
||||||
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
|
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):
|
def update(self, user: User, connection: SCIMProviderUser):
|
||||||
"""Update existing user"""
|
"""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}"
|
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):
|
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||||
"""SCIM 2.0 provider to create users and groups in external applications"""
|
"""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."),
|
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
|
@property
|
||||||
def icon_url(self) -> str | None:
|
def icon_url(self) -> str | None:
|
||||||
return static("authentik/sources/scim.png")
|
return static("authentik/sources/scim.png")
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils.text import slugify
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from requests_mock import Mocker
|
from requests_mock import Mocker
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application, Group, User
|
from authentik.core.models import Application, Group, User
|
||||||
|
from authentik.events.models import SystemTask
|
||||||
from authentik.lib.generators import generate_id
|
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.models import SCIMMapping, SCIMProvider
|
||||||
from authentik.providers.scim.tasks import scim_sync, sync_tasks
|
from authentik.providers.scim.tasks import scim_sync, sync_tasks
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
@ -330,3 +333,59 @@ class SCIMUserTests(TestCase):
|
|||||||
"userName": uid,
|
"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)
|
error_desc = self.get_request_arg("error_description", None)
|
||||||
return {"error": error_desc or error or _("No token received.")}
|
return {"error": error_desc or error or _("No token received.")}
|
||||||
args = {
|
args = {
|
||||||
"client_id": self.get_client_id(),
|
|
||||||
"client_secret": self.get_client_secret(),
|
|
||||||
"redirect_uri": callback,
|
"redirect_uri": callback,
|
||||||
"code": code,
|
"code": code,
|
||||||
"grant_type": "authorization_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)
|
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
||||||
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
||||||
continue
|
continue
|
||||||
config = well_known_config.json()
|
config: dict = well_known_config.json()
|
||||||
try:
|
try:
|
||||||
dirty = False
|
dirty = False
|
||||||
source_attr_key = (
|
source_attr_key = (
|
||||||
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
|
|||||||
for source_attr, config_key in source_attr_key:
|
for source_attr, config_key in source_attr_key:
|
||||||
# Check if we're actually changing anything to only
|
# Check if we're actually changing anything to only
|
||||||
# save when something has changed
|
# 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
|
dirty = True
|
||||||
setattr(source, source_attr, config[config_key])
|
setattr(source, source_attr, config[config_key])
|
||||||
except (IndexError, KeyError) as exc:
|
except (IndexError, KeyError) as exc:
|
||||||
|
@ -25,8 +25,10 @@ class RedditOAuth2Client(UserprofileHeaderAuthClient):
|
|||||||
|
|
||||||
def get_access_token(self, **request_kwargs):
|
def get_access_token(self, **request_kwargs):
|
||||||
"Fetch access token from callback request."
|
"Fetch access token from callback request."
|
||||||
auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret)
|
request_kwargs["auth"] = HTTPBasicAuth(
|
||||||
return super().get_access_token(auth=auth)
|
self.source.consumer_key, self.source.consumer_secret
|
||||||
|
)
|
||||||
|
return super().get_access_token(**request_kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RedditOAuth2Callback(OAuthCallback):
|
class RedditOAuth2Callback(OAuthCallback):
|
||||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.exceptions import StageInvalidException
|
from authentik.flows.exceptions import StageInvalidException
|
||||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||||
@ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
|||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
return "ak-stage-authenticator-email-form"
|
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
|
@property
|
||||||
def backend_class(self) -> type[BaseEmailBackend]:
|
def backend_class(self) -> type[BaseEmailBackend]:
|
||||||
"""Get the email backend class to use"""
|
"""Get the email backend class to use"""
|
||||||
|
@ -299,12 +299,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
|||||||
data={"component": "ak-stage-authenticator-email", "code": device.token},
|
data={"component": "ak-stage-authenticator-email", "code": device.token},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
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):
|
def test_model_properties_and_methods(self):
|
||||||
"""Test model properties"""
|
"""Test model properties"""
|
||||||
@ -331,7 +325,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
|||||||
self.stage.send(device)
|
self.stage.send(device)
|
||||||
|
|
||||||
def test_email_tasks(self):
|
def test_email_tasks(self):
|
||||||
|
|
||||||
email_send_mock = MagicMock()
|
email_send_mock = MagicMock()
|
||||||
with patch(
|
with patch(
|
||||||
"authentik.stages.email.tasks.send_mails",
|
"authentik.stages.email.tasks.send_mails",
|
||||||
|
@ -146,5 +146,10 @@
|
|||||||
"name": "LogMeOnce",
|
"name": "LogMeOnce",
|
||||||
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjA0OCAyMDQ4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTSAwIDAgTCAtNSAxIEwgLTE1IDQgTCAtMzggMTEgTCAtNTcgMTggTCAtNzYgMjkgTCAtOTAgMzggTCAtMTA5IDUyIEwgLTExNiA1OSBMIC0xMjcgNjggTCAtMTQwIDgxIEwgLTE0OCA4OCBMIC0xNDkgOTEgTCAtMTUxIDkxIEwgLTE2MSAxMDEgTCAtMTY5IDEwOCBMIC0xNjkgMTEwIEwgLTE3MSAxMTAgTCAtMTgwIDExOSBMIC0xODggMTI2IEwgLTIwMSAxMzcgTCAtMjEyIDE0NiBMIC0yMjYgMTU3IEwgLTIzOSAxNjcgTCAtMjUwIDE3NSBMIC0yNzAgMTg4IEwgLTI5MSAyMDIgTCAtMzA5IDIxMiBMIC0zMzAgMjI0IEwgLTM1MyAyMzUgTCAtMzg2IDI1MCBMIC00MDcgMjU5IEwgLTQ0OSAyNzMgTCAtNDcyIDI4MSBMIC01MDIgMjg5IEwgLTU1MSAyOTggTCAtNjE1IDMwOSBMIC02NDcgMzE2IEwgLTY4MSAzMjQgTCAtNjk4IDMzMCBMIC03MTQgMzM4IEwgLTczNSAzNTEgTCAtNzQ1IDM1OCBMIC03NTYgMzY3IEwgLTc2NCAzNzUgTCAtNzY2IDM3OSBMIC03NjggMzc5IEwgLTc3MyAzODUgTCAtNzgxIDM5NSBMIC03OTEgNDEwIEwgLTgwMSA0MjggTCAtODEwIDQ0NyBMIC04MTYgNDY1IEwgLTgyMiA1MDAgTCAtODI3IDU0NSBMIC04MzAgNTkyIEwgLTgzMiA2NTMgTCAtODMyIDcxMiBMIC04MzEgNzQyIEwgLTgyOCA3OTQgTCAtODIyIDg2NiBMIC04MTYgOTI4IEwgLTgxMiA5NTcgTCAtODAwIDEwMjAgTCAtNzg5IDEwNzIgTCAtNzgzIDEwOTggTCAtNzY1IDExNjIgTCAtNzUxIDEyMDcgTCAtNzQ0IDEyMjUgTCAtNzM0IDEyNTQgTCAtNzIwIDEyOTAgTCAtNzA0IDEzMjggTCAtNjg5IDEzNjEgTCAtNjY4IDE0MDMgTCAtNjUzIDE0MzEgTCAtNjM5IDE0NTYgTCAtNjIwIDE0ODggTCAtNjA3IDE1MDkgTCAtNTk0IDE1MjkgTCAtNTg0IDE1NDQgTCAtNTcxIDE1NjMgTCAtNTU4IDE1ODEgTCAtNTQ0IDE2MDAgTCAtNTMzIDE2MTUgTCAtNTIwIDE2MzIgTCAtNTA3IDE2NDggTCAtNDc5IDE2ODIgTCAtNDcwIDE2OTMgTCAtNDYwIDE3MDQgTCAtNDQ5IDE3MTYgTCAtNDQyIDE3MjQgTCAtNDMyIDE3MzQgTCAtNDI1IDE3NDIgTCAtMzY4IDE3OTkgTCAtMzUxIDE4MTUgTCAtMzM1IDE4MzAgTCAtMzI3IDE4MzcgTCAtMzE0IDE4NDkgTCAtMjg4IDE4NzEgTCAtMjcxIDE4ODUgTCAtMjYxIDE4OTMgTCAtMjQ0IDE5MDcgTCAtMjI4IDE5MTkgTCAtMjE1IDE5MjkgTCAtMTk2IDE5NDMgTCAtMTg0IDE5NTIgTCAtMTY4IDE5NjMgTCAtMTQ4IDE5NzcgTCAtMTMzIDE5ODcgTCAtMTEyIDIwMDAgTCAtODggMjAxMyBMIC03MiAyMDIxIEwgLTUyIDIwMzAgTCAtMzIgMjAzNyBMIDEyIDIwNDggTCA2MCAyMDQ4IEwgNjEgMjA0NyBMIDczIDIwNDQgTCAxMDAgMjAzNyBMIDEyMSAyMDI5IEwgMTQ5IDIwMTUgTCAxNjcgMjAwNSBMIDE4NiAxOTk0IEwgMjEwIDE5NzkgTCAyMzEgMTk2NSBMIDI2NiAxOTQxIEwgMjg0IDE5MjggTCAyOTUgMTkyMCBMIDMwNiAxOTExIEwgMzI0IDE4OTggTCAzMzggMTg4NyBMIDM0OSAxODc3IEwgMzU4IDE4NzAgTCAzNjkgMTg2MCBMIDM4MCAxODUxIEwgMzgwIDE4NDkgTCAzODIgMTg0OSBMIDM4NSAxODQ2IEwgMzk2IDE4MzcgTCA0MDMgMTgzMCBMIDQxMSAxODIzIEwgNDE4IDE4MTYgTCA0MjYgMTgwOSBMIDQ3NSAxNzYwIEwgNDc3IDE3NTYgTCA0NzkgMTc1NiBMIDQ4NyAxNzQ3IEwgNTAzIDE3MzAgTCA1MTAgMTcyMiBMIDUxOSAxNzEyIEwgNTI2IDE3MDQgTCA1MzggMTY5MSBMIDU0NyAxNjgwIEwgNTU4IDE2NjcgTCA1NjYgMTY1NiBMIDU3OSAxNjQwIEwgNTkwIDE2MjYgTCA2MDYgMTYwNSBMIDYyMSAxNTgzIEwgNjMxIDE1NjkgTCA2NDUgMTU0OSBMIDY1NSAxNTMzIEwgNjcwIDE1MTAgTCA2ODUgMTQ4NSBMIDY5NiAxNDY3IEwgNzA2IDE0NDkgTCA3MzAgMTQwNSBMIDc0NSAxMzc0IEwgNzYyIDEzMzggTCA3NzQgMTMxMCBMIDc5MiAxMjY3IEwgODExIDEyMTYgTCA4MjcgMTE2NiBMIDg0MiAxMTE3IEwgODU4IDEwNTEgTCA4NzAgOTk2IEwgODc1IDk2NyBMIDg4MSA5MTcgTCA4ODYgODgyIEwgODkxIDg0NiBMIDg5NCA4MTIgTCA4OTYgNzc0IEwgODk2IDY1NSBMIDg5NSA2MzAgTCA4OTIgNTkzIEwgODg4IDU1OSBMIDg3NyA0NzAgTCA4NzIgNDQ1IEwgODY1IDQyNiBMIDg1NyA0MTEgTCA4NDcgMzk1IEwgODM0IDM3OCBMIDgxNCAzNTggTCA4MDAgMzQ3IEwgNzg1IDMzNyBMIDc2NiAzMjcgTCA3NTAgMzIxIEwgNzI3IDMxNSBMIDY5MyAzMDggTCA2NTAgMzAxIEwgNTk3IDI5MiBMIDU3MCAyODYgTCA1NDggMjgwIEwgNTE1IDI3MCBMIDQ4NyAyNjEgTCA0NTkgMjUwIEwgNDM4IDI0MSBMIDQxNCAyMzAgTCAzODIgMjEzIEwgMzU4IDE5OSBMIDM0NyAxOTIgTCAzMzAgMTgwIEwgMzEyIDE2NyBMIDMwMCAxNTcgTCAyODYgMTQ2IEwgMjcxIDEzMyBMIDI2MyAxMjYgTCAyNTEgMTE1IEwgMjQwIDEwNiBMIDIyOCA5NSBMIDIwMCA3MSBMIDE4OSA2MiBMIDE3NCA1MCBMIDE1OCAzOCBMIDE0MiAyNyBMIDEyNyAxOSBMIDExNSAxNCBMIDk1IDggTCA3MSAwIEwgMCAwIHogTSAzMCA2MjIgTCA1MSA2MjMgTCA3OSA2MjcgTCAxMDAgNjMzIEwgMTIxIDY0MSBMIDE0MSA2NTIgTCAxNTcgNjYzIEwgMTcyIDY3NiBMIDE4NiA2OTAgTCAxODYgNjkyIEwgMTg4IDY5MiBMIDIwNiA3MTYgTCAyMTYgNzM0IEwgMjIxIDc0NSBMIDIyNiA3NTggTCAyMzMgNzg0IEwgMjM2IDc5OCBMIDIzNyA4MDggTCAyMzcgODQwIEwgMjMzIDg2NSBMIDIyNiA4OTEgTCAyMTkgOTA5IEwgMjA5IDkyOCBMIDE5NSA5NDkgTCAxODYgOTU5IEwgMTc5IDk2NyBMIDE2NiA5ODAgTCAxNTUgOTg5IEwgMTQyIDk5OCBMIDEyNiAxMDA3IEwgMTEzIDEwMTIgTCAxMTQgMTAyNCBMIDEyMiAxMDYwIEwgMTM3IDExMjMgTCAxNTYgMTIwOCBMIDE3MCAxMjcxIEwgMTc0IDEyOTYgTCAxNzYgMTMxMCBMIDE3NiAxMzM1IEwgMTczIDEzNDkgTCAxNjYgMTM1OSBMIDE1NyAxMzY3IEwgMTQ2IDEzNzIgTCAxNDAgMTM3NCBMIDExOSAxMzc2IEwgNTEgMTM3NiBMIC0zMiAxMzc3IEwgLTY0IDEzNzcgTCAtNzggMTM3NCBMIC04NSAxMzcwIEwgLTk4IDEzNTkgTCAtMTA2IDEzNDkgTCAtMTEwIDEzNDEgTCAtMTEyIDEzMzMgTCAtMTEyIDEzMTggTCAtMTA3IDEyODggTCAtMTAwIDEyNTYgTCAtODUgMTE5NiBMIC02OSAxMTI0IEwgLTU3IDEwNzIgTCAtNTAgMTAzNiBMIC00NyAxMDExIEwgLTUzIDEwMDkgTCAtNjkgMTAwMSBMIC04MSA5OTQgTCAtOTMgOTg1IEwgLTEwMyA5NzYgTCAtMTExIDk2OSBMIC0xMjAgOTYwIEwgLTEzMSA5NDYgTCAtMTQyIDkyOSBMIC0xNTIgOTEwIEwgLTE2MCA4OTAgTCAtMTY2IDg3MCBMIC0xNjkgODUzIEwgLTE3MCA4NDQgTCAtMTcwIDgxNCBMIC0xNjcgNzk0IEwgLTE2MSA3NjcgTCAtMTU0IDc0NiBMIC0xMzcgNzEzIEwgLTEyNiA2OTggTCAtMTE5IDY5MCBMIC0xMDggNjc4IEwgLTkzIDY2NSBMIC03NyA2NTMgTCAtNTYgNjQxIEwgLTM4IDYzMyBMIC0xNiA2MjcgTCAzIDYyNCBMIDMwIDYyMiB6ICIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkxKSIgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==",
|
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjA0OCAyMDQ4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTSAwIDAgTCAtNSAxIEwgLTE1IDQgTCAtMzggMTEgTCAtNTcgMTggTCAtNzYgMjkgTCAtOTAgMzggTCAtMTA5IDUyIEwgLTExNiA1OSBMIC0xMjcgNjggTCAtMTQwIDgxIEwgLTE0OCA4OCBMIC0xNDkgOTEgTCAtMTUxIDkxIEwgLTE2MSAxMDEgTCAtMTY5IDEwOCBMIC0xNjkgMTEwIEwgLTE3MSAxMTAgTCAtMTgwIDExOSBMIC0xODggMTI2IEwgLTIwMSAxMzcgTCAtMjEyIDE0NiBMIC0yMjYgMTU3IEwgLTIzOSAxNjcgTCAtMjUwIDE3NSBMIC0yNzAgMTg4IEwgLTI5MSAyMDIgTCAtMzA5IDIxMiBMIC0zMzAgMjI0IEwgLTM1MyAyMzUgTCAtMzg2IDI1MCBMIC00MDcgMjU5IEwgLTQ0OSAyNzMgTCAtNDcyIDI4MSBMIC01MDIgMjg5IEwgLTU1MSAyOTggTCAtNjE1IDMwOSBMIC02NDcgMzE2IEwgLTY4MSAzMjQgTCAtNjk4IDMzMCBMIC03MTQgMzM4IEwgLTczNSAzNTEgTCAtNzQ1IDM1OCBMIC03NTYgMzY3IEwgLTc2NCAzNzUgTCAtNzY2IDM3OSBMIC03NjggMzc5IEwgLTc3MyAzODUgTCAtNzgxIDM5NSBMIC03OTEgNDEwIEwgLTgwMSA0MjggTCAtODEwIDQ0NyBMIC04MTYgNDY1IEwgLTgyMiA1MDAgTCAtODI3IDU0NSBMIC04MzAgNTkyIEwgLTgzMiA2NTMgTCAtODMyIDcxMiBMIC04MzEgNzQyIEwgLTgyOCA3OTQgTCAtODIyIDg2NiBMIC04MTYgOTI4IEwgLTgxMiA5NTcgTCAtODAwIDEwMjAgTCAtNzg5IDEwNzIgTCAtNzgzIDEwOTggTCAtNzY1IDExNjIgTCAtNzUxIDEyMDcgTCAtNzQ0IDEyMjUgTCAtNzM0IDEyNTQgTCAtNzIwIDEyOTAgTCAtNzA0IDEzMjggTCAtNjg5IDEzNjEgTCAtNjY4IDE0MDMgTCAtNjUzIDE0MzEgTCAtNjM5IDE0NTYgTCAtNjIwIDE0ODggTCAtNjA3IDE1MDkgTCAtNTk0IDE1MjkgTCAtNTg0IDE1NDQgTCAtNTcxIDE1NjMgTCAtNTU4IDE1ODEgTCAtNTQ0IDE2MDAgTCAtNTMzIDE2MTUgTCAtNTIwIDE2MzIgTCAtNTA3IDE2NDggTCAtNDc5IDE2ODIgTCAtNDcwIDE2OTMgTCAtNDYwIDE3MDQgTCAtNDQ5IDE3MTYgTCAtNDQyIDE3MjQgTCAtNDMyIDE3MzQgTCAtNDI1IDE3NDIgTCAtMzY4IDE3OTkgTCAtMzUxIDE4MTUgTCAtMzM1IDE4MzAgTCAtMzI3IDE4MzcgTCAtMzE0IDE4NDkgTCAtMjg4IDE4NzEgTCAtMjcxIDE4ODUgTCAtMjYxIDE4OTMgTCAtMjQ0IDE5MDcgTCAtMjI4IDE5MTkgTCAtMjE1IDE5MjkgTCAtMTk2IDE5NDMgTCAtMTg0IDE5NTIgTCAtMTY4IDE5NjMgTCAtMTQ4IDE5NzcgTCAtMTMzIDE5ODcgTCAtMTEyIDIwMDAgTCAtODggMjAxMyBMIC03MiAyMDIxIEwgLTUyIDIwMzAgTCAtMzIgMjAzNyBMIDEyIDIwNDggTCA2MCAyMDQ4IEwgNjEgMjA0NyBMIDczIDIwNDQgTCAxMDAgMjAzNyBMIDEyMSAyMDI5IEwgMTQ5IDIwMTUgTCAxNjcgMjAwNSBMIDE4NiAxOTk0IEwgMjEwIDE5NzkgTCAyMzEgMTk2NSBMIDI2NiAxOTQxIEwgMjg0IDE5MjggTCAyOTUgMTkyMCBMIDMwNiAxOTExIEwgMzI0IDE4OTggTCAzMzggMTg4NyBMIDM0OSAxODc3IEwgMzU4IDE4NzAgTCAzNjkgMTg2MCBMIDM4MCAxODUxIEwgMzgwIDE4NDkgTCAzODIgMTg0OSBMIDM4NSAxODQ2IEwgMzk2IDE4MzcgTCA0MDMgMTgzMCBMIDQxMSAxODIzIEwgNDE4IDE4MTYgTCA0MjYgMTgwOSBMIDQ3NSAxNzYwIEwgNDc3IDE3NTYgTCA0NzkgMTc1NiBMIDQ4NyAxNzQ3IEwgNTAzIDE3MzAgTCA1MTAgMTcyMiBMIDUxOSAxNzEyIEwgNTI2IDE3MDQgTCA1MzggMTY5MSBMIDU0NyAxNjgwIEwgNTU4IDE2NjcgTCA1NjYgMTY1NiBMIDU3OSAxNjQwIEwgNTkwIDE2MjYgTCA2MDYgMTYwNSBMIDYyMSAxNTgzIEwgNjMxIDE1NjkgTCA2NDUgMTU0OSBMIDY1NSAxNTMzIEwgNjcwIDE1MTAgTCA2ODUgMTQ4NSBMIDY5NiAxNDY3IEwgNzA2IDE0NDkgTCA3MzAgMTQwNSBMIDc0NSAxMzc0IEwgNzYyIDEzMzggTCA3NzQgMTMxMCBMIDc5MiAxMjY3IEwgODExIDEyMTYgTCA4MjcgMTE2NiBMIDg0MiAxMTE3IEwgODU4IDEwNTEgTCA4NzAgOTk2IEwgODc1IDk2NyBMIDg4MSA5MTcgTCA4ODYgODgyIEwgODkxIDg0NiBMIDg5NCA4MTIgTCA4OTYgNzc0IEwgODk2IDY1NSBMIDg5NSA2MzAgTCA4OTIgNTkzIEwgODg4IDU1OSBMIDg3NyA0NzAgTCA4NzIgNDQ1IEwgODY1IDQyNiBMIDg1NyA0MTEgTCA4NDcgMzk1IEwgODM0IDM3OCBMIDgxNCAzNTggTCA4MDAgMzQ3IEwgNzg1IDMzNyBMIDc2NiAzMjcgTCA3NTAgMzIxIEwgNzI3IDMxNSBMIDY5MyAzMDggTCA2NTAgMzAxIEwgNTk3IDI5MiBMIDU3MCAyODYgTCA1NDggMjgwIEwgNTE1IDI3MCBMIDQ4NyAyNjEgTCA0NTkgMjUwIEwgNDM4IDI0MSBMIDQxNCAyMzAgTCAzODIgMjEzIEwgMzU4IDE5OSBMIDM0NyAxOTIgTCAzMzAgMTgwIEwgMzEyIDE2NyBMIDMwMCAxNTcgTCAyODYgMTQ2IEwgMjcxIDEzMyBMIDI2MyAxMjYgTCAyNTEgMTE1IEwgMjQwIDEwNiBMIDIyOCA5NSBMIDIwMCA3MSBMIDE4OSA2MiBMIDE3NCA1MCBMIDE1OCAzOCBMIDE0MiAyNyBMIDEyNyAxOSBMIDExNSAxNCBMIDk1IDggTCA3MSAwIEwgMCAwIHogTSAzMCA2MjIgTCA1MSA2MjMgTCA3OSA2MjcgTCAxMDAgNjMzIEwgMTIxIDY0MSBMIDE0MSA2NTIgTCAxNTcgNjYzIEwgMTcyIDY3NiBMIDE4NiA2OTAgTCAxODYgNjkyIEwgMTg4IDY5MiBMIDIwNiA3MTYgTCAyMTYgNzM0IEwgMjIxIDc0NSBMIDIyNiA3NTggTCAyMzMgNzg0IEwgMjM2IDc5OCBMIDIzNyA4MDggTCAyMzcgODQwIEwgMjMzIDg2NSBMIDIyNiA4OTEgTCAyMTkgOTA5IEwgMjA5IDkyOCBMIDE5NSA5NDkgTCAxODYgOTU5IEwgMTc5IDk2NyBMIDE2NiA5ODAgTCAxNTUgOTg5IEwgMTQyIDk5OCBMIDEyNiAxMDA3IEwgMTEzIDEwMTIgTCAxMTQgMTAyNCBMIDEyMiAxMDYwIEwgMTM3IDExMjMgTCAxNTYgMTIwOCBMIDE3MCAxMjcxIEwgMTc0IDEyOTYgTCAxNzYgMTMxMCBMIDE3NiAxMzM1IEwgMTczIDEzNDkgTCAxNjYgMTM1OSBMIDE1NyAxMzY3IEwgMTQ2IDEzNzIgTCAxNDAgMTM3NCBMIDExOSAxMzc2IEwgNTEgMTM3NiBMIC0zMiAxMzc3IEwgLTY0IDEzNzcgTCAtNzggMTM3NCBMIC04NSAxMzcwIEwgLTk4IDEzNTkgTCAtMTA2IDEzNDkgTCAtMTEwIDEzNDEgTCAtMTEyIDEzMzMgTCAtMTEyIDEzMTggTCAtMTA3IDEyODggTCAtMTAwIDEyNTYgTCAtODUgMTE5NiBMIC02OSAxMTI0IEwgLTU3IDEwNzIgTCAtNTAgMTAzNiBMIC00NyAxMDExIEwgLTUzIDEwMDkgTCAtNjkgMTAwMSBMIC04MSA5OTQgTCAtOTMgOTg1IEwgLTEwMyA5NzYgTCAtMTExIDk2OSBMIC0xMjAgOTYwIEwgLTEzMSA5NDYgTCAtMTQyIDkyOSBMIC0xNTIgOTEwIEwgLTE2MCA4OTAgTCAtMTY2IDg3MCBMIC0xNjkgODUzIEwgLTE3MCA4NDQgTCAtMTcwIDgxNCBMIC0xNjcgNzk0IEwgLTE2MSA3NjcgTCAtMTU0IDc0NiBMIC0xMzcgNzEzIEwgLTEyNiA2OTggTCAtMTE5IDY5MCBMIC0xMDggNjc4IEwgLTkzIDY2NSBMIC03NyA2NTMgTCAtNTYgNjQxIEwgLTM4IDYzMyBMIC0xNiA2MjcgTCAzIDYyNCBMIDMwIDYyMiB6ICIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkxKSIgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==",
|
||||||
"icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyMDQ4IDIwNDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5OTEpIiBkPSJtMCAwaDcxbDI0IDggMjAgNiAxMiA1IDE1IDggMTYgMTEgMTYgMTIgMTUgMTIgMTEgOSAyOCAyNCAxMiAxMSAxMSA5IDEyIDExIDggNyAxNSAxMyAxNCAxMSAxMiAxMCAxOCAxMyAxNyAxMiAxMSA3IDI0IDE0IDMyIDE3IDI0IDExIDIxIDkgMjggMTEgMjggOSAzMyAxMCAyMiA2IDI3IDYgNTMgOSA0MyA3IDM0IDcgMjMgNiAxNiA2IDE5IDEwIDE1IDEwIDE0IDExIDIwIDIwIDEzIDE3IDEwIDE2IDggMTUgNyAxOSA1IDI1IDExIDg5IDQgMzQgMyAzNyAxIDI1djExOWwtMiAzOC0zIDM0LTUgMzYtNSAzNS02IDUwLTUgMjktMTIgNTUtMTYgNjYtMTUgNDktMTYgNTAtMTkgNTEtMTggNDMtMTIgMjgtMTcgMzYtMTUgMzEtMjQgNDQtMTAgMTgtMTEgMTgtMTUgMjUtMTUgMjMtMTAgMTYtMTQgMjAtMTAgMTQtMTUgMjItMTYgMjEtMTEgMTQtMTMgMTYtOCAxMS0xMSAxMy05IDExLTEyIDEzLTcgOC05IDEwLTcgOC0xNiAxNy04IDloLTJsLTIgNC00OSA0OS04IDctNyA3LTggNy03IDctMTEgOS0zIDNoLTJ2MmwtMTEgOS0xMSAxMC05IDctMTEgMTAtMTQgMTEtMTggMTMtMTEgOS0xMSA4LTE4IDEzLTM1IDI0LTIxIDE0LTI0IDE1LTE5IDExLTE4IDEwLTI4IDE0LTIxIDgtMjcgNy0xMiAzLTEgMWgtNDhsLTQ0LTExLTIwLTctMjAtOS0xNi04LTI0LTEzLTIxLTEzLTE1LTEwLTIwLTE0LTE2LTExLTEyLTktMTktMTQtMTMtMTAtMTYtMTItMTctMTQtMTAtOC0xNy0xNC0yNi0yMi0xMy0xMi04LTctMTYtMTUtMTctMTYtNTctNTctNy04LTEwLTEwLTctOC0xMS0xMi0xMC0xMS05LTExLTI4LTM0LTEzLTE2LTEzLTE3LTExLTE1LTE0LTE5LTEzLTE4LTEzLTE5LTEwLTE1LTEzLTIwLTEzLTIxLTE5LTMyLTE0LTI1LTE1LTI4LTIxLTQyLTE1LTMzLTE2LTM4LTE0LTM2LTEwLTI5LTctMTgtMTQtNDUtMTgtNjQtNi0yNi0xMS01Mi0xMi02My00LTI5LTYtNjItNi03Mi0zLTUyLTEtMzB2LTU5bDItNjEgMy00NyA1LTQ1IDYtMzUgNi0xOCA5LTE5IDEwLTE4IDEwLTE1IDgtMTAgNS02aDJsMi00IDgtOCAxMS05IDEwLTcgMjEtMTMgMTYtOCAxNy02IDM0LTggMzItNyA2NC0xMSA0OS05IDMwLTggMjMtOCA0Mi0xNCAyMS05IDMzLTE1IDIzLTExIDIxLTEyIDE4LTEwIDIxLTE0IDIwLTEzIDExLTggMTMtMTAgMTQtMTEgMTEtOSAxMy0xMSA4LTcgOS05aDJ2LTJsOC03IDEwLTEwaDJsMS0zIDgtNyAxMy0xMyAxMS05IDctNyAxOS0xNCAxNC05IDE5LTExIDE5LTcgMjMtNyAxMC0zeiIgZmlsbD0iI0YxODQyOSIvPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDIxLDYyMikiIGQ9Im0wIDAgMjEgMSAyOCA0IDIxIDYgMjEgOCAyMCAxMSAxNiAxMSAxNSAxMyAxNCAxNHYyaDJsMTggMjQgMTAgMTggNSAxMSA1IDEzIDcgMjYgMyAxNCAxIDEwdjMybC00IDI1LTcgMjYtNyAxOC0xMCAxOS0xNCAyMS05IDEwLTcgOC0xMyAxMy0xMSA5LTEzIDktMTYgOS0xMyA1IDEgMTIgOCAzNiAxNSA2MyAxOSA4NSAxNCA2MyA0IDI1IDIgMTR2MjVsLTMgMTQtNyAxMC05IDgtMTEgNS02IDItMjEgMmgtNjhsLTgzIDFoLTMybC0xNC0zLTctNC0xMy0xMS04LTEwLTQtOC0yLTh2LTE1bDUtMzAgNy0zMiAxNS02MCAxNi03MiAxMi01MiA3LTM2IDMtMjUtNi0yLTE2LTgtMTItNy0xMi05LTEwLTktOC03LTktOS0xMS0xNC0xMS0xNy0xMC0xOS04LTIwLTYtMjAtMy0xNy0xLTl2LTMwbDMtMjAgNi0yNyA3LTIxIDE3LTMzIDExLTE1IDctOCAxMS0xMiAxNS0xMyAxNi0xMiAyMS0xMiAxOC04IDIyLTYgMTktM3oiIGZpbGw9IiNGRUZGRkUiLz4KPC9zdmc+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.flows.models import Stage
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -74,8 +75,10 @@ class EmailStage(Stage):
|
|||||||
default=False, help_text=_("Activate users upon completion of stage.")
|
default=False, help_text=_("Activate users upon completion of stage.")
|
||||||
)
|
)
|
||||||
|
|
||||||
token_expiry = models.IntegerField(
|
token_expiry = models.TextField(
|
||||||
default=30, help_text=_("Time in minutes the token sent is valid.")
|
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")
|
subject = models.TextField(default="authentik")
|
||||||
template = models.TextField(default=EmailTemplates.PASSWORD_RESET)
|
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.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
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.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -73,8 +74,8 @@ class EmailStageView(ChallengeStageView):
|
|||||||
"""Get token"""
|
"""Get token"""
|
||||||
pending_user = self.get_pending_user()
|
pending_user = self.get_pending_user()
|
||||||
current_stage: EmailStage = self.executor.current_stage
|
current_stage: EmailStage = self.executor.current_stage
|
||||||
valid_delta = timedelta(
|
valid_delta = timedelta_from_string(current_stage.token_expiry) + timedelta(
|
||||||
minutes=current_stage.token_expiry + 1
|
minutes=1
|
||||||
) # + 1 because django timesince always rounds down
|
) # + 1 because django timesince always rounds down
|
||||||
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
|
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
|
||||||
# Don't check for validity here, we only care if the token exists
|
# Don't check for validity here, we only care if the token exists
|
||||||
|
@ -5,7 +5,7 @@ entries:
|
|||||||
- attrs:
|
- attrs:
|
||||||
designation: stage_configuration
|
designation: stage_configuration
|
||||||
name: default-authenticator-totp-setup
|
name: default-authenticator-totp-setup
|
||||||
title: Setup Two-Factor authentication
|
title: Set up Two-Factor authentication
|
||||||
authentication: require_authenticated
|
authentication: require_authenticated
|
||||||
identifiers:
|
identifiers:
|
||||||
slug: default-authenticator-totp-setup
|
slug: default-authenticator-totp-setup
|
||||||
|
@ -57,7 +57,7 @@ entries:
|
|||||||
use_ssl: false
|
use_ssl: false
|
||||||
timeout: 10
|
timeout: 10
|
||||||
from_address: system@authentik.local
|
from_address: system@authentik.local
|
||||||
token_expiry: 30
|
token_expiry: minutes=30
|
||||||
subject: authentik
|
subject: authentik
|
||||||
template: email/password_reset.html
|
template: email/password_reset.html
|
||||||
activate_user_on_success: true
|
activate_user_on_success: true
|
||||||
|
@ -6661,6 +6661,16 @@
|
|||||||
"title": "Token",
|
"title": "Token",
|
||||||
"description": "Authentication 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": {
|
"exclude_users_service_account": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Exclude users service account"
|
"title": "Exclude users service account"
|
||||||
@ -6669,6 +6679,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"title": "Filter group"
|
"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": []
|
"required": []
|
||||||
@ -11364,11 +11379,10 @@
|
|||||||
"title": "From address"
|
"title": "From address"
|
||||||
},
|
},
|
||||||
"token_expiry": {
|
"token_expiry": {
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"minimum": -2147483648,
|
"minLength": 1,
|
||||||
"maximum": 2147483647,
|
|
||||||
"title": "Token expiry",
|
"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": {
|
"subject": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -14196,6 +14210,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Default group email domain"
|
"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": []
|
"required": []
|
||||||
@ -14344,6 +14363,11 @@
|
|||||||
"suspend"
|
"suspend"
|
||||||
],
|
],
|
||||||
"title": "Group delete action"
|
"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": []
|
"required": []
|
||||||
|
16
go.mod
16
go.mod
@ -6,7 +6,7 @@ toolchain go1.24.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
beryju.io/ldap v0.1.0
|
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/getsentry/sentry-go v0.31.1
|
||||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.10
|
||||||
@ -22,17 +22,17 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||||
github.com/pires/go-proxyproto v0.8.0
|
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/redis/go-redis/v9 v9.7.1
|
||||||
github.com/sethvargo/go-envconfig v1.1.1
|
github.com/sethvargo/go-envconfig v1.1.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/wwt/guac v1.3.2
|
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/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.27.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.11.0
|
golang.org/x/sync v0.12.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
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 v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.1 // indirect
|
google.golang.org/protobuf v1.36.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.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/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/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/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.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
|
||||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
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.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 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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.2025021.4 h1:KFap2KW+8CwhOxjBkRnRB4flvuHEMw24+fZei9dOhzw=
|
||||||
goauthentik.io/api/v3 v3.2025020.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.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.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-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-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-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-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-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.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.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.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.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1001.0",
|
"aws-cdk": "^2.1004.0",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aws-cdk": {
|
"node_modules/aws-cdk": {
|
||||||
"version": "2.1001.0",
|
"version": "2.1004.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1001.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1004.0.tgz",
|
||||||
"integrity": "sha512-Wp6fKNXcxBm+f8U1GkLV4gEgqq1pu5uwyDCMBg7ZB/6CtP+PsD/mPhuKyMULNWucDvYN8oy70XLOkMnxa3NWFw==",
|
"integrity": "sha512-3E5ICmSc7ZCZCwLX7NY+HFmmdUYgRaL+67h/BDoDQmkhx9StC8wG4xgzHFY9k8WQS0+ib/MP28f2d9yzHtQLlQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1001.0",
|
"aws-cdk": "^2.1004.0",
|
||||||
"cross-env": "^7.0.3"
|
"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
|
# 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:
|
if CONFIG.get("secret_key") is None or len(CONFIG.get("secret_key")) == 0:
|
||||||
CONFIG.log("info", "----------------------------------------------------------------------")
|
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", "----------------------------------------------------------------------")
|
CONFIG.log("info", "----------------------------------------------------------------------")
|
||||||
sysexit(1)
|
sysexit(1)
|
||||||
check_postgres()
|
check_postgres()
|
||||||
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -865,6 +865,12 @@ msgstr ""
|
|||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr ""
|
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
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
msgid "Starting full provider sync"
|
msgid "Starting full provider sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -879,6 +885,10 @@ msgstr ""
|
|||||||
msgid "Syncing page {page} of groups"
|
msgid "Syncing page {page} of groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
msgid "Dropping mutating request due to dry run"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/lib/sync/outgoing/tasks.py
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Stopping sync due to error: {error}"
|
msgid "Stopping sync due to error: {error}"
|
||||||
@ -1365,6 +1375,14 @@ msgstr ""
|
|||||||
msgid "ES256 (Asymmetric Encryption)"
|
msgid "ES256 (Asymmetric Encryption)"
|
||||||
msgstr ""
|
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
|
#: authentik/providers/oauth2/models.py
|
||||||
msgid "Scope used by the client"
|
msgid "Scope used by the client"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1865,6 +1883,18 @@ msgstr ""
|
|||||||
msgid "SAML Providers from Metadata"
|
msgid "SAML Providers from Metadata"
|
||||||
msgstr ""
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1873,6 +1903,14 @@ msgstr ""
|
|||||||
msgid "Authentication token"
|
msgid "Authentication token"
|
||||||
msgstr ""
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2517,6 +2555,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/stages/authenticator_email/models.py
|
#: 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)."
|
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2855,10 +2894,6 @@ msgstr ""
|
|||||||
msgid "Activate users upon completion of stage."
|
msgid "Activate users upon completion of stage."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
|
||||||
msgid "Time in minutes the token sent is valid."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stage"
|
msgid "Email Stage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
Binary file not shown.
@ -9,9 +9,9 @@
|
|||||||
# Kyllian Delaye-Maillot, 2023
|
# Kyllian Delaye-Maillot, 2023
|
||||||
# Manuel Viens, 2023
|
# Manuel Viens, 2023
|
||||||
# Mordecai, 2023
|
# Mordecai, 2023
|
||||||
# Charles Leclerc, 2024
|
|
||||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||||
# Tina, 2024
|
# Tina, 2024
|
||||||
|
# Charles Leclerc, 2025
|
||||||
# Marc Schmitt, 2025
|
# Marc Schmitt, 2025
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -19,7 +19,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: Marc Schmitt, 2025\n"
|
"Last-Translator: Marc Schmitt, 2025\n"
|
||||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\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"
|
msgid "Invalid next URL"
|
||||||
msgstr "URL suivante invalide"
|
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
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
msgid "Starting full provider sync"
|
msgid "Starting full provider sync"
|
||||||
msgstr "Démarrage d'une synchronisation complète du fournisseur"
|
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"
|
msgid "Syncing page {page} of groups"
|
||||||
msgstr "Synchronisation de la page {page} de groupes"
|
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
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Stopping sync due to error: {error}"
|
msgid "Stopping sync due to error: {error}"
|
||||||
@ -1510,6 +1522,14 @@ msgstr "RS256 (chiffrement asymétrique)"
|
|||||||
msgid "ES256 (Asymmetric Encryption)"
|
msgid "ES256 (Asymmetric Encryption)"
|
||||||
msgstr "ES256 (Chiffrement Asymétrique)"
|
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
|
#: authentik/providers/oauth2/models.py
|
||||||
msgid "Scope used by the client"
|
msgid "Scope used by the client"
|
||||||
msgstr "Portées utilisées par le 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"
|
msgid "SAML Providers from Metadata"
|
||||||
msgstr "Fournisseurs SAML depuis métadonnées"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
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"
|
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"
|
msgid "Authentication token"
|
||||||
msgstr "Jeton d'authentification"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr "Fournisseur SCIM"
|
msgstr "Fournisseur SCIM"
|
||||||
@ -2777,6 +2819,7 @@ msgstr ""
|
|||||||
"les paramètres de connexion ci-dessous seront ignorés."
|
"les paramètres de connexion ci-dessous seront ignorés."
|
||||||
|
|
||||||
#: authentik/stages/authenticator_email/models.py
|
#: 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)."
|
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Durée de validité du jeton envoyé (Format : hours=3,minutes=17,seconds=300)."
|
"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."
|
msgid "Activate users upon completion of stage."
|
||||||
msgstr "Activer les utilisateurs à la complétion de l'étape."
|
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
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stage"
|
msgid "Email Stage"
|
||||||
msgstr "Étape Email"
|
msgstr "Étape Email"
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -7,7 +7,7 @@
|
|||||||
# Chen Zhikai, 2022
|
# Chen Zhikai, 2022
|
||||||
# 刘松, 2022
|
# 刘松, 2022
|
||||||
# Tianhao Chai <cth451@gmail.com>, 2024
|
# Tianhao Chai <cth451@gmail.com>, 2024
|
||||||
# Jens L. <jens@goauthentik.io>, 2024
|
# Jens L. <jens@goauthentik.io>, 2025
|
||||||
# deluxghost, 2025
|
# deluxghost, 2025
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -15,7 +15,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2025\n"
|
"Last-Translator: deluxghost, 2025\n"
|
||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||||
@ -878,6 +878,12 @@ msgstr "流程令牌"
|
|||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr "无效的 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
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
msgid "Starting full provider sync"
|
msgid "Starting full provider sync"
|
||||||
msgstr "开始全量提供程序同步"
|
msgstr "开始全量提供程序同步"
|
||||||
@ -892,6 +898,10 @@ msgstr "正在同步用户页面 {page}"
|
|||||||
msgid "Syncing page {page} of groups"
|
msgid "Syncing page {page} of groups"
|
||||||
msgstr "正在同步群组页面 {page}"
|
msgstr "正在同步群组页面 {page}"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
msgid "Dropping mutating request due to dry run"
|
||||||
|
msgstr "由于启用了试运行,已放弃变更请求"
|
||||||
|
|
||||||
#: authentik/lib/sync/outgoing/tasks.py
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Stopping sync due to error: {error}"
|
msgid "Stopping sync due to error: {error}"
|
||||||
@ -1389,6 +1399,14 @@ msgstr "RS256(非对称加密)"
|
|||||||
msgid "ES256 (Asymmetric Encryption)"
|
msgid "ES256 (Asymmetric Encryption)"
|
||||||
msgstr "ES256(非对称加密)"
|
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
|
#: authentik/providers/oauth2/models.py
|
||||||
msgid "Scope used by the client"
|
msgid "Scope used by the client"
|
||||||
msgstr "客户端使用的作用域"
|
msgstr "客户端使用的作用域"
|
||||||
@ -1891,6 +1909,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
|||||||
msgid "SAML Providers from Metadata"
|
msgid "SAML Providers from Metadata"
|
||||||
msgstr "来自元数据的 SAML 提供程序"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||||
@ -1899,6 +1929,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
|||||||
msgid "Authentication token"
|
msgid "Authentication token"
|
||||||
msgstr "身份验证令牌"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr "SCIM 提供程序"
|
msgstr "SCIM 提供程序"
|
||||||
@ -2553,6 +2591,7 @@ msgid ""
|
|||||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||||
|
|
||||||
#: authentik/stages/authenticator_email/models.py
|
#: 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)."
|
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||||
msgstr "发出令牌有效的时间(格式: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."
|
msgid "Activate users upon completion of stage."
|
||||||
msgstr "完成阶段后激活用户。"
|
msgstr "完成阶段后激活用户。"
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
|
||||||
msgid "Time in minutes the token sent is valid."
|
|
||||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stage"
|
msgid "Email Stage"
|
||||||
msgstr "电子邮件阶段"
|
msgstr "电子邮件阶段"
|
||||||
|
Binary file not shown.
@ -6,7 +6,7 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Chen Zhikai, 2022
|
# Chen Zhikai, 2022
|
||||||
# 刘松, 2022
|
# 刘松, 2022
|
||||||
# Jens L. <jens@goauthentik.io>, 2024
|
# Jens L. <jens@goauthentik.io>, 2025
|
||||||
# deluxghost, 2025
|
# deluxghost, 2025
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2025\n"
|
"Last-Translator: deluxghost, 2025\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -877,6 +877,12 @@ msgstr "流程令牌"
|
|||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr "无效的 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
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
msgid "Starting full provider sync"
|
msgid "Starting full provider sync"
|
||||||
msgstr "开始全量提供程序同步"
|
msgstr "开始全量提供程序同步"
|
||||||
@ -891,6 +897,10 @@ msgstr "正在同步用户页面 {page}"
|
|||||||
msgid "Syncing page {page} of groups"
|
msgid "Syncing page {page} of groups"
|
||||||
msgstr "正在同步群组页面 {page}"
|
msgstr "正在同步群组页面 {page}"
|
||||||
|
|
||||||
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
|
msgid "Dropping mutating request due to dry run"
|
||||||
|
msgstr "由于启用了试运行,已放弃变更请求"
|
||||||
|
|
||||||
#: authentik/lib/sync/outgoing/tasks.py
|
#: authentik/lib/sync/outgoing/tasks.py
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Stopping sync due to error: {error}"
|
msgid "Stopping sync due to error: {error}"
|
||||||
@ -1388,6 +1398,14 @@ msgstr "RS256(非对称加密)"
|
|||||||
msgid "ES256 (Asymmetric Encryption)"
|
msgid "ES256 (Asymmetric Encryption)"
|
||||||
msgstr "ES256(非对称加密)"
|
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
|
#: authentik/providers/oauth2/models.py
|
||||||
msgid "Scope used by the client"
|
msgid "Scope used by the client"
|
||||||
msgstr "客户端使用的作用域"
|
msgstr "客户端使用的作用域"
|
||||||
@ -1890,6 +1908,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
|||||||
msgid "SAML Providers from Metadata"
|
msgid "SAML Providers from Metadata"
|
||||||
msgstr "来自元数据的 SAML 提供程序"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||||
@ -1898,6 +1928,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
|||||||
msgid "Authentication token"
|
msgid "Authentication token"
|
||||||
msgstr "身份验证令牌"
|
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
|
#: authentik/providers/scim/models.py
|
||||||
msgid "SCIM Provider"
|
msgid "SCIM Provider"
|
||||||
msgstr "SCIM 提供程序"
|
msgstr "SCIM 提供程序"
|
||||||
@ -2552,6 +2590,7 @@ msgid ""
|
|||||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||||
|
|
||||||
#: authentik/stages/authenticator_email/models.py
|
#: 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)."
|
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||||
msgstr "发出令牌有效的时间(格式: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."
|
msgid "Activate users upon completion of stage."
|
||||||
msgstr "完成阶段后激活用户。"
|
msgstr "完成阶段后激活用户。"
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
|
||||||
msgid "Time in minutes the token sent is valid."
|
|
||||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stage"
|
msgid "Email Stage"
|
||||||
msgstr "电子邮件阶段"
|
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 = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
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]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = ['py312']
|
target-version = ['py312']
|
||||||
@ -123,7 +149,9 @@ kubernetes = "*"
|
|||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
msgraph-sdk = "*"
|
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 = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
psycopg = { extras = ["c"], version = "*" }
|
psycopg = { extras = ["c"], version = "*" }
|
||||||
|
82
schema.yml
82
schema.yml
@ -35146,7 +35146,7 @@ paths:
|
|||||||
- in: query
|
- in: query
|
||||||
name: token_expiry
|
name: token_expiry
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: string
|
||||||
- in: query
|
- in: query
|
||||||
name: use_global_settings
|
name: use_global_settings
|
||||||
schema:
|
schema:
|
||||||
@ -41582,6 +41582,12 @@ components:
|
|||||||
- confidential
|
- confidential
|
||||||
- public
|
- public
|
||||||
type: string
|
type: string
|
||||||
|
CompatibilityModeEnum:
|
||||||
|
enum:
|
||||||
|
- default
|
||||||
|
- aws
|
||||||
|
- slack
|
||||||
|
type: string
|
||||||
Config:
|
Config:
|
||||||
type: object
|
type: object
|
||||||
description: Serialize authentik Config into DRF Object
|
description: Serialize authentik Config into DRF Object
|
||||||
@ -42774,10 +42780,8 @@ components:
|
|||||||
format: email
|
format: email
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
token_expiry:
|
token_expiry:
|
||||||
type: integer
|
type: string
|
||||||
maximum: 2147483647
|
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||||
minimum: -2147483648
|
|
||||||
description: Time in minutes the token sent is valid.
|
|
||||||
subject:
|
subject:
|
||||||
type: string
|
type: string
|
||||||
template:
|
template:
|
||||||
@ -42833,10 +42837,9 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
token_expiry:
|
token_expiry:
|
||||||
type: integer
|
type: string
|
||||||
maximum: 2147483647
|
minLength: 1
|
||||||
minimum: -2147483648
|
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||||
description: Time in minutes the token sent is valid.
|
|
||||||
subject:
|
subject:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -44154,6 +44157,10 @@ components:
|
|||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
default_group_email_domain:
|
default_group_email_domain:
|
||||||
type: string
|
type: string
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- assigned_backchannel_application_name
|
- assigned_backchannel_application_name
|
||||||
- assigned_backchannel_application_slug
|
- assigned_backchannel_application_slug
|
||||||
@ -44317,6 +44324,10 @@ components:
|
|||||||
default_group_email_domain:
|
default_group_email_domain:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- credentials
|
- credentials
|
||||||
- default_group_email_domain
|
- default_group_email_domain
|
||||||
@ -46397,6 +46408,10 @@ components:
|
|||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
group_delete_action:
|
group_delete_action:
|
||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- assigned_backchannel_application_name
|
- assigned_backchannel_application_name
|
||||||
- assigned_backchannel_application_slug
|
- assigned_backchannel_application_slug
|
||||||
@ -46557,6 +46572,10 @@ components:
|
|||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
group_delete_action:
|
group_delete_action:
|
||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- client_id
|
- client_id
|
||||||
- client_secret
|
- client_secret
|
||||||
@ -50373,10 +50392,9 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
token_expiry:
|
token_expiry:
|
||||||
type: integer
|
type: string
|
||||||
maximum: 2147483647
|
minLength: 1
|
||||||
minimum: -2147483648
|
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||||
description: Time in minutes the token sent is valid.
|
|
||||||
subject:
|
subject:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -50679,6 +50697,10 @@ components:
|
|||||||
default_group_email_domain:
|
default_group_email_domain:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
PatchedGroupKerberosSourceConnectionRequest:
|
PatchedGroupKerberosSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: OAuth Group-Source connection Serializer
|
description: OAuth Group-Source connection Serializer
|
||||||
@ -51260,6 +51282,10 @@ components:
|
|||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
group_delete_action:
|
group_delete_action:
|
||||||
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
$ref: '#/components/schemas/OutgoingSyncDeleteAction'
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
PatchedNotificationRequest:
|
PatchedNotificationRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Notification Serializer
|
description: Notification Serializer
|
||||||
@ -52421,12 +52447,21 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: Authentication token
|
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:
|
exclude_users_service_account:
|
||||||
type: boolean
|
type: boolean
|
||||||
filter_group:
|
filter_group:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
PatchedSCIMSourceGroupRequest:
|
PatchedSCIMSourceGroupRequest:
|
||||||
type: object
|
type: object
|
||||||
description: SCIMSourceGroup Serializer
|
description: SCIMSourceGroup Serializer
|
||||||
@ -55817,12 +55852,21 @@ components:
|
|||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
description: Authentication token
|
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:
|
exclude_users_service_account:
|
||||||
type: boolean
|
type: boolean
|
||||||
filter_group:
|
filter_group:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- assigned_backchannel_application_name
|
- assigned_backchannel_application_name
|
||||||
- assigned_backchannel_application_slug
|
- assigned_backchannel_application_slug
|
||||||
@ -55903,12 +55947,21 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: Authentication token
|
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:
|
exclude_users_service_account:
|
||||||
type: boolean
|
type: boolean
|
||||||
filter_group:
|
filter_group:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: When enabled, provider will not modify or create objects in
|
||||||
|
the remote system.
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- token
|
- token
|
||||||
@ -57105,6 +57158,9 @@ components:
|
|||||||
sync_object_id:
|
sync_object_id:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
override_dry_run:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
required:
|
required:
|
||||||
- sync_object_id
|
- sync_object_id
|
||||||
- sync_object_model
|
- 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"""
|
"""Generate config for development"""
|
||||||
|
|
||||||
from yaml import safe_dump
|
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 { execFileSync } from "child_process";
|
||||||
import * as chokidar from "chokidar";
|
|
||||||
import esbuild from "esbuild";
|
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 { globSync } from "glob";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { cwd } from "process";
|
import { cwd } from "process";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import { fileURLToPath } from "url";
|
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 + "../";
|
let authentikProjectRoot = __dirname + "../";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the package.json file in the root folder, as it has the current version information.
|
// Use the package.json file in the root folder, as it has the current version information.
|
||||||
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
}).replace("\n", "");
|
}).replace("\n", "");
|
||||||
} catch (_exc) {
|
} catch (_error) {
|
||||||
// We probably don't have a .git folder, which could happen in container builds
|
// 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 = {
|
const definitions = Object.fromEntries(
|
||||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
Array.from(environmentVars).map(([key, value]) => {
|
||||||
"process.env.CWD": JSON.stringify(cwd()),
|
return [`process.env.${key}`, JSON.stringify(value)];
|
||||||
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
|
}),
|
||||||
};
|
);
|
||||||
|
|
||||||
// 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
|
* All is magic is just to make sure the assets are copied into the right places. This is a very
|
||||||
// use, and using globSync instead of globby since we already had globSync lying around thanks to
|
* stripped down version of what the rollup-copy-plugin does, without any of the features we don't
|
||||||
// Typescript. If there's a third argument in an array entry, it's used to replace the internal path
|
* use, and using globSync instead of globby since we already had globSync lying around thanks to
|
||||||
// before concatenating it all together as the destination target.
|
* 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 = [
|
* @type {Array<[string, string, string?]>}
|
||||||
|
*/
|
||||||
|
const assetsFileMappings = [
|
||||||
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
||||||
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
||||||
["src/custom.css", "."],
|
["src/custom.css", "."],
|
||||||
@ -48,28 +57,47 @@ const otherFiles = [
|
|||||||
["./icons/*", "./assets/icons"],
|
["./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) {
|
function nameCopyTarget(src, dest, strip) {
|
||||||
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
|
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
|
||||||
return [src, target];
|
return [src, target];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [source, rawdest, strip] of otherFiles) {
|
for (const [source, rawdest, strip] of assetsFileMappings) {
|
||||||
const matchedPaths = globSync(source);
|
const matchedPaths = globSync(source);
|
||||||
const dest = path.join("dist", rawdest);
|
const dest = path.join("dist", rawdest);
|
||||||
|
|
||||||
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
|
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
|
||||||
|
|
||||||
for (const [src, dest] of copyTargets) {
|
for (const [src, dest] of copyTargets) {
|
||||||
if (isFile(src)) {
|
if (isFile(src)) {
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
fs.copyFileSync(src, dest);
|
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.
|
* @typedef {[source: string, destination: string]} EntryPoint
|
||||||
// Ordered by largest to smallest interface to build even faster
|
*/
|
||||||
const interfaces = [
|
|
||||||
|
/**
|
||||||
|
* 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"],
|
["admin/AdminInterface/AdminInterface.ts", "admin"],
|
||||||
["user/UserInterface.ts", "user"],
|
["user/UserInterface.ts", "user"],
|
||||||
["flow/FlowInterface.ts", "flow"],
|
["flow/FlowInterface.ts", "flow"],
|
||||||
@ -79,11 +107,14 @@ const interfaces = [
|
|||||||
["polyfill/poly.ts", "."],
|
["polyfill/poly.ts", "."],
|
||||||
];
|
];
|
||||||
|
|
||||||
const baseArgs = {
|
/**
|
||||||
|
* @satisfies {import("esbuild").BuildOptions}
|
||||||
|
*/
|
||||||
|
const BASE_ESBUILD_OPTIONS = {
|
||||||
bundle: true,
|
bundle: true,
|
||||||
write: true,
|
write: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
minify: isProdBuild,
|
minify: NODE_ENV === "production",
|
||||||
splitting: true,
|
splitting: true,
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
external: ["*.woff", "*.woff2"],
|
external: ["*.woff", "*.woff2"],
|
||||||
@ -95,6 +126,7 @@ const baseArgs = {
|
|||||||
},
|
},
|
||||||
define: definitions,
|
define: definitions,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
|
plugins: [],
|
||||||
logOverride: {
|
logOverride: {
|
||||||
/**
|
/**
|
||||||
* HACK: Silences issue originating in ESBuild.
|
* HACK: Silences issue originating in ESBuild.
|
||||||
@ -106,91 +138,144 @@ const baseArgs = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getVersion() {
|
/**
|
||||||
let version = rootPackage.version;
|
* Creates a version ID for the build.
|
||||||
if (process.env[envGitHashKey]) {
|
* @returns {string}
|
||||||
version = `${version}+${process.env[envGitHashKey]}`;
|
*/
|
||||||
|
function composeVersionID() {
|
||||||
|
const { version } = rootPackage;
|
||||||
|
const buildHash = process.env.GIT_BUILD_HASH;
|
||||||
|
|
||||||
|
if (buildHash) {
|
||||||
|
return `${version}+${buildHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildOneSource(source, dest) {
|
/**
|
||||||
const DIST = path.join(__dirname, "./dist", dest);
|
* Build a single entry point.
|
||||||
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
|
*
|
||||||
|
* @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 {
|
return {
|
||||||
const start = Date.now();
|
...BASE_ESBUILD_OPTIONS,
|
||||||
await esbuild.build({
|
entryPoints: [`./src/${source}`],
|
||||||
...baseArgs,
|
entryNames: `[dir]/[name]-${composeVersionID()}`,
|
||||||
entryPoints: [`./src/${source}`],
|
outdir,
|
||||||
entryNames: `[dir]/[name]-${getVersion()}`,
|
...overrides,
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAuthentik(interfaces) {
|
/**
|
||||||
const code = await Promise.allSettled(
|
* Build all entry points in parallel.
|
||||||
interfaces.map(([source, dest]) => buildOneSource(source, dest)),
|
*
|
||||||
|
* @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 doHelp() {
|
||||||
function debouncedBuild() {
|
console.log(`Build the authentik UI
|
||||||
if (timeoutId !== null) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
console.clear();
|
|
||||||
buildAuthentik(interfaces);
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
|
options:
|
||||||
console.log(`Build the authentikUI
|
-w, --watch: Build all ${entryPoints.length} interfaces
|
||||||
|
-p, --proxy: Build only the polyfills and the loading application
|
||||||
options:
|
-h, --help: This help message
|
||||||
-w, --watch: Build all ${interfaces.length} interfaces
|
|
||||||
-p, --proxy: Build only the polyfills and the loading application
|
|
||||||
-h, --help: This help message
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) {
|
async function doWatch() {
|
||||||
console.log("Watching ./src for changes");
|
console.log("Watching all entry points...");
|
||||||
chokidar.watch("./src").on("all", (event, path) => {
|
|
||||||
if (!["add", "change", "unlink"].includes(event)) {
|
const wathcherPorts = await findFreePorts(entryPoints.length);
|
||||||
return;
|
|
||||||
}
|
const buildContexts = await Promise.all(
|
||||||
if (!/(\.css|\.ts|\.js)$/.test(path)) {
|
entryPoints.map((entryPoint, i) => {
|
||||||
return;
|
const port = wathcherPorts[i];
|
||||||
}
|
const serverURL = new URL(`http://localhost:${port}/events`);
|
||||||
debouncedBuild();
|
|
||||||
});
|
return esbuild.context(
|
||||||
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
createEntryPointOptions(entryPoint, {
|
||||||
// There's no watch-for-proxy, sorry.
|
plugins: [
|
||||||
process.exit(
|
...BASE_ESBUILD_OPTIONS.plugins,
|
||||||
await buildAuthentik(
|
buildObserverPlugin({
|
||||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
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",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@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-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -37,11 +37,12 @@
|
|||||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"construct-style-sheets-polyfill": "^3.1.0",
|
"construct-style-sheets-polyfill": "^3.1.0",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"country-flag-icons": "^1.5.13",
|
"country-flag-icons": "^1.5.13",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"guacamole-common-js": "^1.5.0",
|
"guacamole-common-js": "^1.5.0",
|
||||||
@ -89,6 +90,7 @@
|
|||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-lit": "^1.15.0",
|
"eslint-plugin-lit": "^1.15.0",
|
||||||
"eslint-plugin-wc": "^2.1.1",
|
"eslint-plugin-wc": "^2.1.1",
|
||||||
|
"find-free-ports": "^3.1.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"globals": "^15.10.0",
|
"globals": "^15.10.0",
|
||||||
@ -824,9 +826,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime-corejs3": {
|
"node_modules/@babel/runtime-corejs3": {
|
||||||
"version": "7.25.7",
|
"version": "7.26.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.25.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.10.tgz",
|
||||||
"integrity": "sha512-gMmIEhg35sXk9Te5qbGp3W9YKrvLt3HV658/d3odWrHSqT0JeG5OzsJWFHRLiOohRyjRsJc/x03DhJm3i8VJxg==",
|
"integrity": "sha512-uITFQYO68pMEYR46AHgQoyBg7KPPJDAbGn4jUTIRgCFJIp88MIBUianVOplhZDEec07bp9zIyr4Kp0FCyQzmWg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js-pure": "^3.30.2",
|
"core-js-pure": "^3.30.2",
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
@ -1814,9 +1817,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2025.2.1-1740653734",
|
"version": "2025.2.1-1741798605",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.1-1740653734.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.1-1741798605.tgz",
|
||||||
"integrity": "sha512-GRxBt52lgZOvEu7l9DN1lj0L2Q9KUiftrC9MWfaz3dIlw1s+kKzic/NTTlB7AaEsRqw7+i10aI6GkiKAErw2VA=="
|
"integrity": "sha512-Go0Iij1q/imohOSqxj43pvju8D+OFH7iNBBg6FO1ytd9pcHi1QY7/jq1vy1HKWZw7oZ2fY6hZnSe+keRnBA0Fg=="
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/web": {
|
"node_modules/@goauthentik/web": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
@ -9531,13 +9534,13 @@
|
|||||||
"pnpm": ">=8"
|
"pnpm": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chartjs-adapter-moment": {
|
"node_modules/chartjs-adapter-date-fns": {
|
||||||
"version": "1.0.1",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||||
"integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==",
|
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"chart.js": ">=3.0.0",
|
"chart.js": ">=2.8.0",
|
||||||
"moment": "^2.10.2"
|
"date-fns": ">=2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
@ -10769,6 +10772,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
"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"
|
"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": {
|
"node_modules/find-up": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
@ -16572,6 +16591,7 @@
|
|||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
@ -18215,9 +18235,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prismjs": {
|
"node_modules/prismjs": {
|
||||||
"version": "1.29.0",
|
"version": "1.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@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-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -25,11 +25,12 @@
|
|||||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"construct-style-sheets-polyfill": "^3.1.0",
|
"construct-style-sheets-polyfill": "^3.1.0",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"country-flag-icons": "^1.5.13",
|
"country-flag-icons": "^1.5.13",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"guacamole-common-js": "^1.5.0",
|
"guacamole-common-js": "^1.5.0",
|
||||||
@ -77,6 +78,7 @@
|
|||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-lit": "^1.15.0",
|
"eslint-plugin-lit": "^1.15.0",
|
||||||
"eslint-plugin-wc": "^2.1.1",
|
"eslint-plugin-wc": "^2.1.1",
|
||||||
|
"find-free-ports": "^3.1.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"globals": "^15.10.0",
|
"globals": "^15.10.0",
|
||||||
|
@ -90,12 +90,14 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.ws = new WebsocketClient();
|
this.ws = new WebsocketClient();
|
||||||
|
|
||||||
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
||||||
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
||||||
updateURLParams({
|
updateURLParams({
|
||||||
notificationDrawerOpen: this.notificationDrawerOpen,
|
notificationDrawerOpen: this.notificationDrawerOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
||||||
this.apiDrawerOpen = !this.apiDrawerOpen;
|
this.apiDrawerOpen = !this.apiDrawerOpen;
|
||||||
updateURLParams({
|
updateURLParams({
|
||||||
@ -107,6 +109,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
async firstUpdated(): Promise<void> {
|
async firstUpdated(): Promise<void> {
|
||||||
configureSentry(true);
|
configureSentry(true);
|
||||||
this.user = await me();
|
this.user = await me();
|
||||||
|
|
||||||
const canAccessAdmin =
|
const canAccessAdmin =
|
||||||
this.user.user.isSuperuser ||
|
this.user.user.isSuperuser ||
|
||||||
// TODO: somehow add `access_admin_interface` to the API schema
|
// 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 {
|
render(): TemplateResult {
|
||||||
const sidebarClasses = {
|
const sidebarClasses = {
|
||||||
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
|
"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 PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||||
import PFPage from "@patternfly/patternfly/components/Page/page.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 PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
return [
|
return [
|
||||||
PFBase,
|
PFBase,
|
||||||
PFGrid,
|
PFGrid,
|
||||||
|
PFFlex,
|
||||||
PFPage,
|
PFPage,
|
||||||
PFContent,
|
PFContent,
|
||||||
PFDivider,
|
PFDivider,
|
||||||
@ -67,13 +69,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
.card-container {
|
.card-container {
|
||||||
max-height: 10em;
|
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}>
|
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
|
||||||
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
|
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
|
||||||
</ak-page-header>
|
</ak-page-header>
|
||||||
|
|
||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
<!-- row 1 -->
|
${this.renderCards()}
|
||||||
<div
|
<div class="pf-l-grid__item pf-m-9-col pf-m-3-row">
|
||||||
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">
|
|
||||||
<ak-recent-events pageSize="6"></ak-recent-events>
|
<ak-recent-events pageSize="6"></ak-recent-events>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-l-grid__item pf-m-12-col">
|
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||||
<hr class="pf-c-divider" />
|
<ak-quick-actions-card .actions=${this.quickActions}>
|
||||||
|
</ak-quick-actions-card>
|
||||||
</div>
|
</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 -->
|
<!-- row 3 -->
|
||||||
<div
|
<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"
|
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 { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
|
import {
|
||||||
|
APIError,
|
||||||
|
parseAPIResponseError,
|
||||||
|
pluckErrorDetail,
|
||||||
|
} from "@goauthentik/common/errors/network";
|
||||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||||
import { until } from "lit/directives/until.js";
|
import { state } from "lit/decorators.js";
|
||||||
|
|
||||||
import { ResponseError } from "@goauthentik/api";
|
import { ResponseError } from "@goauthentik/api";
|
||||||
|
|
||||||
@ -13,46 +18,143 @@ export interface AdminStatus {
|
|||||||
message?: TemplateResult;
|
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 {
|
export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||||
abstract getPrimaryValue(): Promise<T>;
|
// Current data value state
|
||||||
|
@state()
|
||||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
|
||||||
|
|
||||||
value?: T;
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addEventListener(EVENT_REFRESH, () => {
|
// Proper binding for event handler
|
||||||
this.requestUpdate();
|
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}`;
|
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 {
|
renderInner(): TemplateResult {
|
||||||
return html`<p class="center-value">
|
return html`
|
||||||
${until(
|
<p class="center-value">
|
||||||
this.getPrimaryValue()
|
${
|
||||||
.then((v) => {
|
this.status
|
||||||
this.value = v;
|
? this.renderStatus(this.status) // Status available
|
||||||
return this.getStatus(v);
|
: this.error
|
||||||
})
|
? this.renderError(pluckErrorDetail(this.error)) // Error state
|
||||||
.then((status) => {
|
: this.renderLoading() // Loading state
|
||||||
return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
}
|
||||||
${status.message
|
</p>
|
||||||
? 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>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EventWithContext } from "@goauthentik/common/events";
|
import { EventWithContext } from "@goauthentik/common/events";
|
||||||
import { actionToLabel } from "@goauthentik/common/labels";
|
import { actionToLabel } from "@goauthentik/common/labels";
|
||||||
@ -10,6 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton";
|
|||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { Table, TableColumn } 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 { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -38,6 +39,22 @@ export class RecentEventsCard extends Table<Event> {
|
|||||||
return super.styles.concat(
|
return super.styles.concat(
|
||||||
PFCard,
|
PFCard,
|
||||||
css`
|
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 {
|
||||||
--pf-c-card__title--FontFamily: var(
|
--pf-c-card__title--FontFamily: var(
|
||||||
--pf-global--FontFamily--heading--sans-serif
|
--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--FontSize: var(--pf-global--FontSize--md);
|
||||||
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
|
--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;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@ -68,20 +125,57 @@ export class RecentEventsCard extends Table<Event> {
|
|||||||
</div>`;
|
</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 [
|
return [
|
||||||
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
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),
|
EventUser(item),
|
||||||
html`<div>${getRelativeTime(item.created)}</div>
|
|
||||||
<small>${item.created.toLocaleString()}</small>`,
|
html`<time datetime="${item.created.toISOString()}" class="pf-c-content">
|
||||||
html` <div>${item.clientIp || msg("-")}</div>
|
<div><small>${item.created.toLocaleTimeString()}</small></div>
|
||||||
<small>${EventGeo(item)}</small>`,
|
</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>`,
|
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmpty(): TemplateResult {
|
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
|
||||||
|
if (this.error) {
|
||||||
|
return super.renderEmpty(inner);
|
||||||
|
}
|
||||||
|
|
||||||
return super.renderEmpty(
|
return super.renderEmpty(
|
||||||
html`<ak-empty-state header=${msg("No Events found.")}>
|
html`<ak-empty-state header=${msg("No Events found.")}>
|
||||||
<div slot="body">${msg("No matching events could be found.")}</div>
|
<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 api = new OutpostsApi(DEFAULT_CONFIG);
|
||||||
const outposts = await api.outpostsInstancesList({});
|
const outposts = await api.outpostsInstancesList({});
|
||||||
const outpostStats: SummarizedSyncStatus[] = [];
|
const outpostStats: SummarizedSyncStatus[] = [];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
outposts.results.map(async (element) => {
|
outposts.results.map(async (element) => {
|
||||||
const health = await api.outpostsInstancesHealthList({
|
const health = await api.outpostsInstancesHealthList({
|
||||||
uuid: element.pk || "",
|
uuid: element.pk || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const singleStats: SummarizedSyncStatus = {
|
const singleStats: SummarizedSyncStatus = {
|
||||||
unsynced: 0,
|
unsynced: 0,
|
||||||
healthy: 0,
|
healthy: 0,
|
||||||
@ -42,9 +44,11 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
|||||||
total: health.length,
|
total: health.length,
|
||||||
label: element.name,
|
label: element.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (health.length === 0) {
|
if (health.length === 0) {
|
||||||
singleStats.unsynced += 1;
|
singleStats.unsynced += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
health.forEach((h) => {
|
health.forEach((h) => {
|
||||||
if (h.versionOutdated) {
|
if (h.versionOutdated) {
|
||||||
singleStats.failed += 1;
|
singleStats.failed += 1;
|
||||||
@ -52,11 +56,14 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
|||||||
singleStats.healthy += 1;
|
singleStats.healthy += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
outpostStats.push(singleStats);
|
outpostStats.push(singleStats);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.centerText = outposts.pagination.count.toString();
|
this.centerText = outposts.pagination.count.toString();
|
||||||
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
return outpostStats;
|
return outpostStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
|
|
||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
|
|
||||||
import { Application, CoreApi } from "@goauthentik/api";
|
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
|
||||||
|
|
||||||
import "./ApplicationWizardHint";
|
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>
|
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||||
</ak-forms-modal>`;
|
</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 {
|
declare global {
|
||||||
|
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
|
|||||||
import { ValidationError } from "@goauthentik/api";
|
import { ValidationError } from "@goauthentik/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ApplicationTransactionValidationError,
|
||||||
type ApplicationWizardState,
|
type ApplicationWizardState,
|
||||||
type ApplicationWizardStateUpdate,
|
type ApplicationWizardStateUpdate,
|
||||||
ExtendedValidationError,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class ApplicationWizardStep extends WizardStep {
|
export class ApplicationWizardStep extends WizardStep {
|
||||||
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected removeErrors(
|
protected removeErrors(
|
||||||
keyToDelete: keyof ExtendedValidationError,
|
keyToDelete: keyof ApplicationTransactionValidationError,
|
||||||
): ValidationError | undefined {
|
): ValidationError | undefined {
|
||||||
if (!this.wizard.errors) {
|
if (!this.wizard.errors) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
|||||||
get bindingsAsColumns() {
|
get bindingsAsColumns() {
|
||||||
return this.wizard.bindings.map((binding, index) => {
|
return this.wizard.bindings.map((binding, index) => {
|
||||||
const { order, enabled, timeout } = binding;
|
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)
|
const policy = match(binding)
|
||||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.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 "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
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 { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
@ -33,7 +33,7 @@ import {
|
|||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||||
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
import { ApplicationTransactionValidationError, OneOfProvider } from "../types.js";
|
||||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||||
|
|
||||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||||
@ -131,39 +131,36 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
|
|
||||||
this.state = "running";
|
this.state = "running";
|
||||||
|
|
||||||
return (
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
new CoreApi(DEFAULT_CONFIG)
|
.coreTransactionalApplicationsUpdate({
|
||||||
.coreTransactionalApplicationsUpdate({
|
transactionApplicationRequest: request,
|
||||||
transactionApplicationRequest: request,
|
})
|
||||||
})
|
.then((_response: TransactionApplicationResponse) => {
|
||||||
.then((_response: TransactionApplicationResponse) => {
|
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
this.state = "submitted";
|
||||||
this.state = "submitted";
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
.catch(async (resolution) => {
|
||||||
.catch(async (resolution: any) => {
|
const errors =
|
||||||
const errors = (await parseAPIError(
|
await parseAPIResponseError<ApplicationTransactionValidationError>(resolution);
|
||||||
await resolution,
|
|
||||||
)) as ExtendedValidationError;
|
|
||||||
|
|
||||||
// THIS is a really gross special case; if the user is duplicating the name of
|
// 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.
|
||||||
// an existing provider, the error appears on the `app` (!) error object. We
|
// We have to move that to the `provider.name` error field so it shows up in the right place.
|
||||||
// have to move that to the `provider.name` error field so it shows up in the
|
if (Array.isArray(errors?.app?.provider)) {
|
||||||
// right place.
|
const providerError = errors.app.provider;
|
||||||
if (Array.isArray(errors?.app?.provider)) {
|
errors.provider = errors.provider ?? {};
|
||||||
const providerError = errors.app.provider;
|
errors.provider.name = providerError;
|
||||||
errors.provider = errors.provider ?? {};
|
|
||||||
errors.provider.name = providerError;
|
delete errors.app.provider;
|
||||||
delete errors.app.provider;
|
|
||||||
if (Object.keys(errors.app).length === 0) {
|
if (Object.keys(errors.app).length === 0) {
|
||||||
delete errors.app;
|
delete errors.app;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.handleUpdate({ errors });
|
}
|
||||||
this.state = "reviewing";
|
|
||||||
})
|
this.handleUpdate({ errors });
|
||||||
);
|
this.state = "reviewing";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override handleButton(button: WizardButton) {
|
override handleButton(button: WizardButton) {
|
||||||
@ -232,7 +229,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
||||||
const errors = this.wizard.errors;
|
const errors = this.wizard.errors;
|
||||||
return html` <hr class="pf-c-divider" />
|
return html` <hr class="pf-c-divider" />
|
||||||
${match(errors as ExtendedValidationError)
|
${match(errors as ApplicationTransactionValidationError)
|
||||||
.with(
|
.with(
|
||||||
{ app: P.nonNullable },
|
{ app: P.nonNullable },
|
||||||
() =>
|
() =>
|
||||||
@ -328,7 +325,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
if (!(this.wizard && app && provider)) {
|
if (!(this.wizard && app && provider)) {
|
||||||
throw new Error("Submit step received uninitialized wizard context");
|
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);
|
const keys = Object.keys(this.wizard.errors);
|
||||||
return match([this.state, keys])
|
return match([this.state, keys])
|
||||||
.with(["submitted", P._], () =>
|
.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