mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
6 Commits
github-ci-
...
website/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f107b17bd | ||
|
|
b5439407b7 | ||
|
|
821b74d7c1 | ||
|
|
8963d29ab4 | ||
|
|
699360064e | ||
|
|
3f94f830fc |
81
.github/actions/setup-node/action.yml
vendored
81
.github/actions/setup-node/action.yml
vendored
@@ -1,81 +0,0 @@
|
||||
name: "Setup Node.js and NPM"
|
||||
description: "Sets up Node.js with a specific NPM version via Corepack"
|
||||
inputs:
|
||||
working-directory:
|
||||
description: "Path to the working directory containing the package.json file"
|
||||
required: false
|
||||
default: "."
|
||||
dependencies:
|
||||
required: false
|
||||
description: "List of dependencies to setup"
|
||||
default: "monorepo,working-directory"
|
||||
node-version-file:
|
||||
description: "Path to file containing the Node.js version"
|
||||
required: false
|
||||
default: "package.json"
|
||||
cache-dependency-path:
|
||||
description: "Path to dependency lock file for caching"
|
||||
required: false
|
||||
default: "package-lock.json"
|
||||
cache:
|
||||
description: "Package manager to cache"
|
||||
default: "npm"
|
||||
registry-url:
|
||||
description: "npm registry URL"
|
||||
default: "https://registry.npmjs.org"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js (Corepack bootstrap)
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
node-version-file: ${{ inputs.node-version-file }}
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
# The setup-node action will attempt to create a cache using a version of
|
||||
# npm that may not be compatible with the range specified in package.json.
|
||||
# This can be enabled **after** corepack is installed and the correct npm version is available.
|
||||
package-manager-cache: false
|
||||
- name: Install Corepack
|
||||
working-directory: ${{ github.workspace}}
|
||||
shell: bash
|
||||
run: | #shell
|
||||
node ./scripts/node/lint-runtime.mjs
|
||||
node ./scripts/node/setup-corepack.mjs --force
|
||||
corepack enable
|
||||
- name: Lint Node.js and NPM versions
|
||||
shell: bash
|
||||
run: node ./scripts/node/lint-runtime.mjs
|
||||
- name: Setup Node.js (Monorepo Root)
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
node-version-file: ${{ inputs.node-version-file }}
|
||||
cache: ${{ inputs.cache }}
|
||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
- name: Install monorepo dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'monorepo') }}
|
||||
shell: bash
|
||||
run: | #shell
|
||||
node ./scripts/node/lint-lockfile.mjs
|
||||
corepack npm ci
|
||||
- name: Setup Node.js (Working Directory)
|
||||
if: ${{ contains(inputs.dependencies, 'working-directory') }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
node-version-file: ${{ inputs.working-directory }}/${{ inputs.node-version-file }}
|
||||
cache: ${{ inputs.cache }}
|
||||
cache-dependency-path: ${{ inputs.working-directory }}/${{ inputs.cache-dependency-path }}
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
|
||||
- name: Install working directory dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'working-directory') }}
|
||||
shell: bash
|
||||
run: | # shell
|
||||
corepack install
|
||||
|
||||
echo "node version: $(node --version)"
|
||||
echo "npm version: $(corepack npm --version)"
|
||||
|
||||
node ./scripts/node/lint-lockfile.mjs ${{ inputs.working-directory }}
|
||||
corepack npm ci --prefix ${{ inputs.working-directory }}
|
||||
46
.github/actions/setup/action.yml
vendored
46
.github/actions/setup/action.yml
vendored
@@ -18,24 +18,19 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cleanup apt
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies,
|
||||
'python') }}
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: sudo apt-get remove --purge man-db
|
||||
- name: Install apt deps
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies,
|
||||
'python') }}
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
uses: gerlero/apt-install@f4fa5265092af9e750549565d28c99aec7189639
|
||||
with:
|
||||
packages: libpq-dev openssl libxmlsec1-dev pkg-config gettext krb5-multidev
|
||||
libkrb5-dev heimdal-multidev libclang-dev krb5-kdc krb5-user
|
||||
krb5-admin-server
|
||||
packages: libpq-dev openssl libxmlsec1-dev pkg-config gettext krb5-multidev libkrb5-dev heimdal-multidev libclang-dev krb5-kdc krb5-user krb5-admin-server
|
||||
update: true
|
||||
upgrade: false
|
||||
install-recommends: false
|
||||
- name: Make space on disk
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies,
|
||||
'python') }}
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /tmp/empty/
|
||||
@@ -56,8 +51,7 @@ runs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Setup rust (stable)
|
||||
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies,
|
||||
'rust-nightly') }}
|
||||
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
with:
|
||||
rustflags: ""
|
||||
@@ -73,11 +67,27 @@ runs:
|
||||
uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (root, web)
|
||||
- name: Setup node (web)
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: ./.github/actions/setup-node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: "${{ inputs.working-directory }}web/package.json"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}web/package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Setup node (root)
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
node-version-file: "${{ inputs.working-directory }}package.json"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Install Node deps
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: npm ci
|
||||
- name: Setup go
|
||||
if: ${{ contains(inputs.dependencies, 'go') }}
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5
|
||||
@@ -87,17 +97,15 @@ runs:
|
||||
if: ${{ contains(inputs.dependencies, 'runtime') }}
|
||||
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
|
||||
with:
|
||||
key: docker-images-${{ runner.os }}-${{
|
||||
hashFiles('.github/actions/setup/compose.yml', 'Makefile') }}-${{
|
||||
inputs.postgresql_version }}
|
||||
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
||||
- name: Setup dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'runtime') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
corepack npm ci --prefix web
|
||||
docker compose -f .github/actions/setup/compose.yml up -d --wait
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
|
||||
6
.github/actions/setup/compose.yml
vendored
6
.github/actions/setup/compose.yml
vendored
@@ -8,8 +8,14 @@ services:
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
POSTGRES_DB: authentik
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} -h 127.0.0.1"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 60
|
||||
restart: always
|
||||
s3:
|
||||
container_name: s3
|
||||
|
||||
@@ -67,16 +67,6 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: web
|
||||
dependencies: "monorepo"
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
id: push
|
||||
@@ -91,8 +81,7 @@ jobs:
|
||||
${{ steps.ev.outputs.imageBuildArgs }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
platforms: linux/${{ inputs.image_arch }}
|
||||
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames
|
||||
}}:buildcache-${{ inputs.image_arch }}
|
||||
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
|
||||
cache-to: ${{ steps.ev.outputs.cacheTo }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
id: attest
|
||||
|
||||
65
.github/workflows/api-ts-publish.yml
vendored
65
.github/workflows/api-ts-publish.yml
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: API - Publish Typescript client
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "schema.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Required for NPM OIDC trusted publisher
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: web
|
||||
- name: Generate API Client
|
||||
run: make gen-client-ts
|
||||
- name: Publish package
|
||||
working-directory: gen-ts-api/
|
||||
run: |
|
||||
npm i
|
||||
npm publish --tag generated
|
||||
- name: Upgrade /web
|
||||
working-directory: web
|
||||
run: |
|
||||
export VERSION=`node -e 'import mod from "./gen-ts-api/package.json" with { type: "json" };console.log(mod.version);'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Upgrade /web/packages/sfe
|
||||
working-directory: web/packages/sfe
|
||||
run: |
|
||||
export VERSION=`node -e 'import mod from "./gen-ts-api/package.json" with { type: "json" };console.log(mod.version);'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: bump API Client version"
|
||||
title: "web: bump API Client version"
|
||||
body: "web: bump API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||
labels: dependencies
|
||||
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
26
.github/workflows/ci-api-docs.yml
vendored
26
.github/workflows/ci-api-docs.yml
vendored
@@ -22,19 +22,25 @@ jobs:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: website
|
||||
- name: Install Dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: corepack npm run ${{ matrix.command }} --prefix website
|
||||
working-directory: website/
|
||||
run: npm run ${{ matrix.command }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: website
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
with:
|
||||
path: |
|
||||
@@ -48,7 +54,7 @@ jobs:
|
||||
working-directory: website
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: corepack npm run build -w api
|
||||
run: npm run build -w api
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
|
||||
with:
|
||||
name: api-docs
|
||||
@@ -65,9 +71,11 @@ jobs:
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: website
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- name: Deploy Netlify (Production)
|
||||
working-directory: website/api
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
9
.github/workflows/ci-aws-cfn.yml
vendored
9
.github/workflows/ci-aws-cfn.yml
vendored
@@ -24,9 +24,14 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: lifecycle/aws
|
||||
node-version-file: lifecycle/aws/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: lifecycle/aws/package-lock.json
|
||||
- working-directory: lifecycle/aws/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Check changes have been applied
|
||||
run: |
|
||||
uv run make aws-cfn
|
||||
|
||||
38
.github/workflows/ci-docs.yml
vendored
38
.github/workflows/ci-docs.yml
vendored
@@ -24,34 +24,46 @@ jobs:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: website
|
||||
- name: Install dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: corepack npm run ${{ matrix.command }} --prefix website
|
||||
working-directory: website/
|
||||
run: npm run ${{ matrix.command }}
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: production
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
name: Setup Node.js
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: website
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Documentation via Docusaurus
|
||||
run: corepack npm run build --prefix website
|
||||
working-directory: website/
|
||||
run: npm run build
|
||||
build-integrations:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: production
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: website
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Integrations via Docusaurus
|
||||
run: corepack npm run build -w integrations --prefix website
|
||||
working-directory: website/
|
||||
run: npm run build -w integrations
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -92,9 +104,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' &&
|
||||
'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max'
|
||||
|| '' }}
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
|
||||
47
.github/workflows/ci-main.yml
vendored
47
.github/workflows/ci-main.yml
vendored
@@ -73,8 +73,7 @@ jobs:
|
||||
- name: generate API clients
|
||||
run: make gen-clients
|
||||
- name: ensure schema is up-to-date
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json
|
||||
packages/client-go packages/client-rust packages/client-ts
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json packages/client-go packages/client-rust packages/client-ts
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -92,8 +91,7 @@ jobs:
|
||||
outputs:
|
||||
seed: ${{ steps.seed.outputs.seed }}
|
||||
test-migrations-from-stable:
|
||||
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{
|
||||
matrix.run_id }}/5
|
||||
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: test-make-seed
|
||||
@@ -103,7 +101,7 @@ jobs:
|
||||
psql:
|
||||
- 14-alpine
|
||||
- 18-alpine
|
||||
run_id: [ 1, 2, 3, 4, 5 ]
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
@@ -111,13 +109,8 @@ jobs:
|
||||
- name: checkout stable
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
|
||||
cp -R .github ..
|
||||
cp -R scripts ..
|
||||
|
||||
mkdir -p ../packages
|
||||
cp -R packages/logger-js ../packages/logger-js
|
||||
|
||||
# Previous stable tag
|
||||
prev_stable=$(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||
# Current version family based on
|
||||
@@ -125,13 +118,10 @@ jobs:
|
||||
if [[ -n $current_version_family ]]; then
|
||||
prev_stable="version/${current_version_family}"
|
||||
fi
|
||||
|
||||
echo "::notice::Checking out ${prev_stable} as stable version..."
|
||||
git checkout ${prev_stable}
|
||||
|
||||
rm -rf .github/ scripts/ packages/logger-js/
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts .
|
||||
mv ../packages/logger-js ./packages/
|
||||
- name: Setup authentik env (stable)
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -179,7 +169,7 @@ jobs:
|
||||
psql:
|
||||
- 14-alpine
|
||||
- 18-alpine
|
||||
run_id: [ 1, 2, 3, 4, 5 ]
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
@@ -262,22 +252,19 @@ jobs:
|
||||
COMPOSE_PROFILES: ${{ matrix.job.profiles }}
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- uses: ./.github/actions/setup-node
|
||||
- id: cache-web
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
if: contains(matrix.job.profiles, 'selenium')
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json',
|
||||
'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true' && contains(matrix.job.profiles,
|
||||
'selenium')
|
||||
if: steps.cache-web.outputs.cache-hit != 'true' && contains(matrix.job.profiles, 'selenium')
|
||||
working-directory: web
|
||||
run: |
|
||||
corepack npm ci
|
||||
corepack npm run build
|
||||
corepack npm run build:sfe
|
||||
npm ci
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
@@ -315,14 +302,14 @@ jobs:
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**',
|
||||
'web/packages/sfe/src/**') }}-b
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
working-directory: web
|
||||
run: |
|
||||
corepack npm ci --prefix web
|
||||
corepack npm run build --prefix web
|
||||
corepack npm run build:sfe --prefix web
|
||||
npm ci
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run conformance
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
@@ -388,9 +375,7 @@ jobs:
|
||||
uses: ./.github/workflows/_reusable-docker-build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ${{ github.repository == 'goauthentik/authentik-internal' &&
|
||||
'ghcr.io/goauthentik/internal-server' ||
|
||||
'ghcr.io/goauthentik/dev-server' }}
|
||||
image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }}
|
||||
release: false
|
||||
pr-comment:
|
||||
needs:
|
||||
|
||||
22
.github/workflows/ci-outpost.yml
vendored
22
.github/workflows/ci-outpost.yml
vendored
@@ -114,11 +114,8 @@ jobs:
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type
|
||||
}}:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' &&
|
||||
format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max',
|
||||
matrix.type) || '' }}
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
@@ -139,8 +136,8 @@ jobs:
|
||||
- ldap
|
||||
- radius
|
||||
- rac
|
||||
goos: [ linux ]
|
||||
goarch: [ amd64, arm64 ]
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
@@ -148,11 +145,16 @@ jobs:
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
run: corepack npm run build-proxy --prefix web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
set -x
|
||||
|
||||
54
.github/workflows/ci-web.yml
vendored
54
.github/workflows/ci-web.yml
vendored
@@ -15,30 +15,48 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
command:
|
||||
- lint
|
||||
- lint:lockfile
|
||||
- tsc
|
||||
- prettier-check
|
||||
project:
|
||||
- web
|
||||
include:
|
||||
- command: tsc
|
||||
project: web
|
||||
- command: lit-analyse
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: ${{ matrix.project }}/package-lock.json
|
||||
- working-directory: ${{ matrix.project }}/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Lint
|
||||
run: corepack npm run lint --prefix web
|
||||
- name: Check types
|
||||
run: corepack npm run tsc --prefix web
|
||||
- name: Check formatting
|
||||
run: corepack npm run prettier-check --prefix web
|
||||
- name: Lit analyse
|
||||
run: corepack npm run lit-analyse --prefix web
|
||||
|
||||
working-directory: ${{ matrix.project }}/
|
||||
run: npm run ${{ matrix.command }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: build
|
||||
working-directory: web/
|
||||
run: corepack npm run build
|
||||
run: npm run build
|
||||
ci-web-mark:
|
||||
if: always()
|
||||
needs:
|
||||
@@ -55,9 +73,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: test
|
||||
working-directory: web/
|
||||
run: corepack npm run test || exit 0
|
||||
run: npm run test || exit 0
|
||||
|
||||
15
.github/workflows/packages-npm-publish.yml
vendored
15
.github/workflows/packages-npm-publish.yml
vendored
@@ -3,7 +3,7 @@ name: Packages - Publish NPM packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths:
|
||||
- packages/tsconfig/**
|
||||
- packages/eslint-config/**
|
||||
@@ -35,19 +35,22 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: ${{ matrix.package }}
|
||||
node-version-file: ${{ matrix.package }}/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
with:
|
||||
files: |
|
||||
${{ matrix.package }}/package.json
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Publish package
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ${{ matrix.package }}
|
||||
run: |
|
||||
corepack npm ci
|
||||
corepack npm run build
|
||||
corepack npm publish
|
||||
npm ci
|
||||
npm run build
|
||||
npm publish
|
||||
|
||||
28
.github/workflows/release-publish.yml
vendored
28
.github/workflows/release-publish.yml
vendored
@@ -3,7 +3,7 @@ name: Release - On publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published, created ]
|
||||
types: [published, created]
|
||||
|
||||
jobs:
|
||||
build-server:
|
||||
@@ -87,9 +87,11 @@ jobs:
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -142,16 +144,22 @@ jobs:
|
||||
- proxy
|
||||
- ldap
|
||||
- radius
|
||||
goos: [ linux, darwin ]
|
||||
goarch: [ amd64, arm64 ]
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: ./.github/actions/setup-node
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
with:
|
||||
working-directory: web
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Install web dependencies
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
@@ -167,10 +175,8 @@ jobs:
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{
|
||||
matrix.goarch }}
|
||||
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{
|
||||
matrix.goarch }}
|
||||
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
tag: ${{ github.ref }}
|
||||
upload-aws-cfn-template:
|
||||
permissions:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,8 +14,6 @@ media
|
||||
# Node
|
||||
|
||||
node_modules
|
||||
corepack.tgz
|
||||
.corepack
|
||||
|
||||
.cspellcache
|
||||
cspell-report.*
|
||||
|
||||
66
Makefile
66
Makefile
@@ -106,9 +106,8 @@ migrate: ## Run the Authentik Django server's migrations
|
||||
|
||||
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
||||
|
||||
aws-cfn: node-install
|
||||
corepack npm install --prefix lifecycle/aws
|
||||
$(UV) run corepack npm run aws-cfn --prefix lifecycle/aws
|
||||
aws-cfn:
|
||||
cd lifecycle/aws && npm i && $(UV) run npm run aws-cfn
|
||||
|
||||
run-server: ## Run the main authentik server process
|
||||
$(UV) run ak server
|
||||
@@ -129,7 +128,7 @@ core-i18n-extract:
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
install: node-install web-install core-install ## Install all requires dependencies for `node`, `web` and `core`
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
|
||||
@@ -233,46 +232,38 @@ gen-dev-config: ## Generate a local development config file
|
||||
#########################
|
||||
|
||||
node-install: ## Install the necessary libraries to build Node.js packages
|
||||
node ./scripts/node/setup-corepack.mjs
|
||||
node ./scripts/node/lint-runtime.mjs
|
||||
|
||||
node ./scripts/node/lint-runtime.mjs
|
||||
npm ci
|
||||
npm ci --prefix web
|
||||
|
||||
#########################
|
||||
## Web
|
||||
#########################
|
||||
|
||||
web-install: ## Install the necessary libraries to build the Authentik UI
|
||||
node ./scripts/node/lint-runtime.mjs web
|
||||
|
||||
corepack npm ci
|
||||
corepack npm ci --prefix web
|
||||
|
||||
web-build: ## Build the Authentik UI
|
||||
corepack npm run --prefix web build
|
||||
web-build: node-install ## Build the Authentik UI
|
||||
npm run --prefix web build
|
||||
|
||||
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
|
||||
|
||||
web-test: ## Run tests for the Authentik UI
|
||||
corepack npm run --prefix web test
|
||||
npm run --prefix web test
|
||||
|
||||
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
|
||||
corepack npm run --prefix web watch
|
||||
npm run --prefix web watch
|
||||
web-storybook-watch: ## Build and run the storybook documentation server
|
||||
corepack npm run --prefix web storybook
|
||||
npm run --prefix web storybook
|
||||
|
||||
web-lint-fix:
|
||||
corepack npm run --prefix web prettier
|
||||
npm run --prefix web prettier
|
||||
|
||||
web-lint:
|
||||
corepack npm run --prefix web lint
|
||||
corepack npm run --prefix web lit-analyse
|
||||
npm run --prefix web lint
|
||||
npm run --prefix web lit-analyse
|
||||
|
||||
web-check-compile:
|
||||
corepack npm run --prefix web tsc
|
||||
npm run --prefix web tsc
|
||||
|
||||
web-i18n-extract:
|
||||
corepack npm run --prefix web extract-locales
|
||||
npm run --prefix web extract-locales
|
||||
|
||||
#########################
|
||||
## Docs
|
||||
@@ -280,40 +271,35 @@ web-i18n-extract:
|
||||
|
||||
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
|
||||
|
||||
docs-install: node-install ## Install the necessary libraries to build the Authentik documentation
|
||||
node ./scripts/node/lint-runtime.mjs
|
||||
|
||||
corepack npm ci
|
||||
|
||||
corepack npm ci --prefix website
|
||||
docs-install:
|
||||
npm ci --prefix website
|
||||
|
||||
docs-lint-fix: lint-spellcheck
|
||||
corepack npm run --prefix website prettier
|
||||
npm run --prefix website prettier
|
||||
|
||||
docs-build:
|
||||
node ./scripts/node/lint-runtime.mjs website
|
||||
corepack npm run --prefix website build
|
||||
npm run --prefix website build
|
||||
|
||||
docs-watch: ## Build and watch the topics documentation
|
||||
corepack npm run --prefix website start
|
||||
npm run --prefix website start
|
||||
|
||||
integrations: docs-lint-fix integrations-build ## Fix formatting issues in the integrations source code, lint the code, and compile it
|
||||
|
||||
integrations-build:
|
||||
corepack npm run --prefix website -w integrations build
|
||||
npm run --prefix website -w integrations build
|
||||
|
||||
integrations-watch: ## Build and watch the Integrations documentation
|
||||
corepack npm run --prefix website -w integrations start
|
||||
npm run --prefix website -w integrations start
|
||||
|
||||
docs-api-build:
|
||||
corepack npm run --prefix website -w api build
|
||||
npm run --prefix website -w api build
|
||||
|
||||
docs-api-watch: ## Build and watch the API documentation
|
||||
corepack npm run --prefix website -w api generate
|
||||
corepack npm run --prefix website -w api start
|
||||
npm run --prefix website -w api generate
|
||||
npm run --prefix website -w api start
|
||||
|
||||
docs-api-clean: ## Clean generated API documentation
|
||||
corepack npm run --prefix website -w api build:api:clean
|
||||
npm run --prefix website -w api build:api:clean
|
||||
|
||||
#########################
|
||||
## Docker
|
||||
|
||||
@@ -126,7 +126,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||
|
||||
def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str | None = None):
|
||||
"""Check for individual permissions for each model in a blueprint"""
|
||||
for entry in blueprint.entries:
|
||||
for entry in blueprint.iter_entries():
|
||||
full_model = entry.get_model(blueprint)
|
||||
app, __, model = full_model.partition(".")
|
||||
perms = [
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Test blueprints v1"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
@@ -42,3 +45,45 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||
# Ensure objects do not exist
|
||||
self.assertFalse(Flow.objects.filter(slug=flow_slug1))
|
||||
self.assertFalse(Flow.objects.filter(slug=flow_slug2))
|
||||
|
||||
def test_enterprise_license_context_unlicensed(self):
|
||||
"""Test enterprise license context defaults to a false boolean when unlicensed."""
|
||||
license_key = LicenseKey("test", 0, "Test license", 0, 0)
|
||||
|
||||
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||
importer = Importer.from_string("""
|
||||
version: 1
|
||||
entries:
|
||||
- identifiers:
|
||||
name: enterprise-test
|
||||
slug: enterprise-test
|
||||
model: authentik_flows.flow
|
||||
conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
""")
|
||||
|
||||
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], False)
|
||||
|
||||
def test_enterprise_license_context_licensed(self):
|
||||
"""Test enterprise license context defaults to a true boolean when licensed."""
|
||||
license_key = LicenseKey("test", 253402300799, "Test license", 1000, 1000)
|
||||
|
||||
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||
importer = Importer.from_string("""
|
||||
version: 1
|
||||
entries:
|
||||
- identifiers:
|
||||
name: enterprise-test
|
||||
slug: enterprise-test
|
||||
model: authentik_flows.flow
|
||||
conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
""")
|
||||
|
||||
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], True)
|
||||
|
||||
@@ -146,9 +146,7 @@ class Importer:
|
||||
try:
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
context["goauthentik.io/enterprise/licensed"] = (
|
||||
LicenseKey.get_total().status().is_valid,
|
||||
)
|
||||
context["goauthentik.io/enterprise/licensed"] = LicenseKey.get_total().status().is_valid
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
return context
|
||||
|
||||
@@ -64,6 +64,7 @@ class BrandSerializer(ModelSerializer):
|
||||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"flow_lockdown",
|
||||
"default_application",
|
||||
"web_certificate",
|
||||
"client_certificates",
|
||||
@@ -117,6 +118,7 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
||||
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
||||
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
||||
flow_lockdown = CharField(source="flow_lockdown.slug", required=False)
|
||||
|
||||
default_locale = CharField(read_only=True)
|
||||
flags = SerializerMethodField()
|
||||
@@ -154,6 +156,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
||||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"flow_lockdown",
|
||||
"web_certificate",
|
||||
"client_certificates",
|
||||
]
|
||||
|
||||
25
authentik/brands/migrations/0012_brand_flow_lockdown.py
Normal file
25
authentik/brands/migrations/0012_brand_flow_lockdown.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-14 02:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_brands", "0011_alter_brand_branding_default_flow_background_and_more"),
|
||||
("authentik_flows", "0031_alter_flow_layout"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="brand",
|
||||
name="flow_lockdown",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="brand_lockdown",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -58,6 +58,9 @@ class Brand(SerializerModel):
|
||||
flow_device_code = models.ForeignKey(
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
|
||||
)
|
||||
flow_lockdown = models.ForeignKey(
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_lockdown"
|
||||
)
|
||||
|
||||
default_application = models.ForeignKey(
|
||||
"authentik_core.Application",
|
||||
|
||||
@@ -563,6 +563,9 @@ class UsersFilter(FilterSet):
|
||||
|
||||
|
||||
class UserViewSet(
|
||||
ConditionalInheritance(
|
||||
"authentik.enterprise.stages.account_lockdown.api.UserAccountLockdownMixin"
|
||||
),
|
||||
ConditionalInheritance("authentik.enterprise.reports.api.reports.ExportMixin"),
|
||||
UsedByMixin,
|
||||
ModelViewSet,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import BooleanField as ModelBooleanField
|
||||
from django.db.models import Case, Q, Value, When
|
||||
from django.db.models import Exists, OuterRef, Q, Subquery
|
||||
from django_filters.rest_framework import BooleanFilter, FilterSet
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
@@ -14,7 +13,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.lifecycle.api.reviews import ReviewSerializer
|
||||
from authentik.enterprise.lifecycle.models import LifecycleIteration, ReviewState
|
||||
from authentik.enterprise.lifecycle.models import LifecycleIteration, LifecycleRule, ReviewState
|
||||
from authentik.enterprise.lifecycle.utils import (
|
||||
ContentTypeField,
|
||||
ReviewerGroupSerializer,
|
||||
@@ -26,20 +25,25 @@ from authentik.enterprise.lifecycle.utils import (
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
|
||||
class RelatedRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
reviewer_groups = ReviewerGroupSerializer(many=True, read_only=True)
|
||||
min_reviewers = IntegerField(read_only=True)
|
||||
reviewers = ReviewerUserSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LifecycleRule
|
||||
fields = ["id", "name", "reviewer_groups", "min_reviewers", "reviewers"]
|
||||
|
||||
|
||||
class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
content_type = ContentTypeField()
|
||||
object_verbose = SerializerMethodField()
|
||||
rule = RelatedRuleSerializer(read_only=True)
|
||||
object_admin_url = SerializerMethodField(read_only=True)
|
||||
grace_period_end = SerializerMethodField(read_only=True)
|
||||
reviews = ReviewSerializer(many=True, read_only=True, source="review_set.all")
|
||||
user_can_review = SerializerMethodField(read_only=True)
|
||||
|
||||
reviewer_groups = ReviewerGroupSerializer(
|
||||
many=True, read_only=True, source="rule.reviewer_groups"
|
||||
)
|
||||
min_reviewers = IntegerField(read_only=True, source="rule.min_reviewers")
|
||||
reviewers = ReviewerUserSerializer(many=True, read_only=True, source="rule.reviewers")
|
||||
|
||||
next_review_date = SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -55,10 +59,8 @@ class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
"grace_period_end",
|
||||
"next_review_date",
|
||||
"reviews",
|
||||
"rule",
|
||||
"user_can_review",
|
||||
"reviewer_groups",
|
||||
"min_reviewers",
|
||||
"reviewers",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -88,43 +90,55 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
|
||||
queryset = LifecycleIteration.objects.all()
|
||||
serializer_class = LifecycleIterationSerializer
|
||||
ordering = ["-opened_on"]
|
||||
ordering_fields = ["state", "content_type__model", "opened_on", "grace_period_end"]
|
||||
ordering_fields = [
|
||||
"state",
|
||||
"content_type__model",
|
||||
"rule__name",
|
||||
"opened_on",
|
||||
"grace_period_end",
|
||||
]
|
||||
filterset_class = LifecycleIterationFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return self.queryset.annotate(
|
||||
user_is_reviewer=Case(
|
||||
When(
|
||||
Q(rule__reviewers=user)
|
||||
| Q(rule__reviewer_groups__in=user.groups.all().with_ancestors()),
|
||||
then=Value(True),
|
||||
),
|
||||
default=Value(False),
|
||||
output_field=ModelBooleanField(),
|
||||
user_is_reviewer=Exists(
|
||||
LifecycleRule.objects.filter(
|
||||
pk=OuterRef("rule_id"),
|
||||
).filter(
|
||||
Q(reviewers=user) | Q(reviewer_groups__in=user.groups.all().with_ancestors())
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
operation_id="lifecycle_iterations_list_latest",
|
||||
responses={200: LifecycleIterationSerializer(many=True)},
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
methods=["get"],
|
||||
url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
|
||||
)
|
||||
def latest_iteration(self, request: Request, content_type: str, object_id: str) -> Response:
|
||||
def latest_iterations(self, request: Request, content_type: str, object_id: str) -> Response:
|
||||
ct = parse_content_type(content_type)
|
||||
try:
|
||||
obj = (
|
||||
self.get_queryset()
|
||||
.filter(
|
||||
content_type__app_label=ct["app_label"],
|
||||
content_type__model=ct["model"],
|
||||
object_id=object_id,
|
||||
)
|
||||
.latest("opened_on")
|
||||
latest_ids_subquery = (
|
||||
LifecycleIteration.objects.filter(
|
||||
rule=OuterRef("rule"),
|
||||
content_type__app_label=ct["app_label"],
|
||||
content_type__model=ct["model"],
|
||||
object_id=object_id,
|
||||
)
|
||||
except LifecycleIteration.DoesNotExist:
|
||||
return Response(status=404)
|
||||
serializer = self.get_serializer(obj)
|
||||
.order_by("-opened_on")
|
||||
.values("id")[:1]
|
||||
)
|
||||
latest_per_rule = LifecycleIteration.objects.filter(
|
||||
content_type__app_label=ct["app_label"],
|
||||
content_type__model=ct["model"],
|
||||
object_id=object_id,
|
||||
).filter(id=Subquery(latest_ids_subquery))
|
||||
serializer = self.get_serializer(latest_per_rule, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
|
||||
@@ -84,23 +84,6 @@ class LifecycleRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
raise ValidationError(
|
||||
{"grace_period": _("Grace period must be shorter than the interval.")}
|
||||
)
|
||||
if "content_type" in attrs or "object_id" in attrs:
|
||||
content_type = attrs.get("content_type", getattr(self.instance, "content_type", None))
|
||||
object_id = attrs.get("object_id", getattr(self.instance, "object_id", None))
|
||||
if content_type is not None and object_id is None:
|
||||
existing = LifecycleRule.objects.filter(
|
||||
content_type=content_type, object_id__isnull=True
|
||||
)
|
||||
if self.instance:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
if existing.exists():
|
||||
raise ValidationError(
|
||||
{
|
||||
"content_type": _(
|
||||
"Only one type-wide rule for each object type is allowed."
|
||||
)
|
||||
}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-05 11:27
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_lifecycle", "0002_alter_lifecycleiteration_opened_on"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="lifecyclerule",
|
||||
name="uniq_lifecycle_rule_ct_null_object",
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="lifecyclerule",
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
@@ -56,14 +56,6 @@ class LifecycleRule(SerializerModel):
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=["content_type"])]
|
||||
unique_together = [["content_type", "object_id"]]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["content_type"],
|
||||
condition=Q(object_id__isnull=True),
|
||||
name="uniq_lifecycle_rule_ct_null_object",
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
@@ -82,12 +74,6 @@ class LifecycleRule(SerializerModel):
|
||||
qs = self.content_type.get_all_objects_for_this_type()
|
||||
if self.object_id:
|
||||
qs = qs.filter(pk=self.object_id)
|
||||
else:
|
||||
qs = qs.exclude(
|
||||
pk__in=LifecycleRule.objects.filter(
|
||||
content_type=self.content_type, object_id__isnull=False
|
||||
).values_list(Cast("object_id", output_field=self._get_pk_field()), flat=True)
|
||||
)
|
||||
return qs
|
||||
|
||||
def _get_stale_iterations(self) -> QuerySet[LifecycleIteration]:
|
||||
@@ -107,8 +93,7 @@ class LifecycleRule(SerializerModel):
|
||||
|
||||
def _get_newly_due_objects(self) -> QuerySet:
|
||||
recent_iteration_ids = LifecycleIteration.objects.filter(
|
||||
content_type=self.content_type,
|
||||
object_id__isnull=False,
|
||||
rule=self,
|
||||
opened_on__gte=start_of_day(
|
||||
timezone.now() + timedelta(days=1) - timedelta_from_string(self.interval)
|
||||
),
|
||||
@@ -214,9 +199,15 @@ class LifecycleIteration(SerializerModel, ManagedModel):
|
||||
}
|
||||
|
||||
def initialize(self):
|
||||
if (self.content_type.app_label, self.content_type.model) == ("authentik_core", "group"):
|
||||
object_label = self.object.name
|
||||
elif (self.content_type.app_label, self.content_type.model) == ("authentik_rbac", "role"):
|
||||
object_label = self.object.name
|
||||
else:
|
||||
object_label = str(self.object)
|
||||
event = Event.new(
|
||||
EventAction.REVIEW_INITIATED,
|
||||
message=_(f"Access review is due for {self.content_type.name} {str(self.object)}"),
|
||||
message=_(f"Access review is due for {self.content_type.name.lower()} {object_label}"),
|
||||
**self._get_event_args(),
|
||||
)
|
||||
event.save()
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from authentik.enterprise.lifecycle.models import LifecycleRule, ReviewState
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
|
||||
|
||||
@receiver(post_save, sender=LifecycleRule)
|
||||
@@ -11,7 +12,9 @@ def post_rule_save(sender, instance: LifecycleRule, created: bool, **_):
|
||||
|
||||
apply_lifecycle_rule.send_with_options(
|
||||
args=(instance.id,),
|
||||
rel_obj=instance,
|
||||
rel_obj=Schedule.objects.get(
|
||||
actor_name="authentik.enterprise.lifecycle.tasks.apply_lifecycle_rules"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ from dramatiq import actor
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.lifecycle.models import LifecycleRule
|
||||
from authentik.events.models import Event, Notification, NotificationTransport
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
|
||||
|
||||
@actor(description=_("Dispatch tasks to validate lifecycle rules."))
|
||||
@actor(description=_("Dispatch tasks to apply lifecycle rules."))
|
||||
def apply_lifecycle_rules():
|
||||
for rule in LifecycleRule.objects.all():
|
||||
apply_lifecycle_rule.send_with_options(
|
||||
args=(rule.id,),
|
||||
rel_obj=rule,
|
||||
rel_obj=Schedule.objects.get(
|
||||
actor_name="authentik.enterprise.lifecycle.tasks.apply_lifecycle_rules"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
@@ -19,6 +20,11 @@ class TestLifecycleRuleAPI(APITestCase):
|
||||
self.content_type = ContentType.objects.get_for_model(Application)
|
||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
config = apps.get_app_config("authentik_tasks_schedules")
|
||||
config._on_startup_callback(None)
|
||||
|
||||
def test_list_rules(self):
|
||||
rule = LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
@@ -190,6 +196,11 @@ class TestIterationAPI(APITestCase):
|
||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||
self.reviewer_group.users.add(self.user)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
config = apps.get_app_config("authentik_tasks_schedules")
|
||||
config._on_startup_callback(None)
|
||||
|
||||
def test_open_iterations(self):
|
||||
rule = LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
@@ -231,7 +242,7 @@ class TestIterationAPI(APITestCase):
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:lifecycleiteration-latest-iteration",
|
||||
"authentik_api:lifecycleiteration-latest-iterations",
|
||||
kwargs={
|
||||
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
||||
"object_id": str(self.app.pk),
|
||||
@@ -239,19 +250,20 @@ class TestIterationAPI(APITestCase):
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["object_id"], str(self.app.pk))
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]["object_id"], str(self.app.pk))
|
||||
|
||||
def test_latest_iteration_not_found(self):
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:lifecycleiteration-latest-iteration",
|
||||
"authentik_api:lifecycleiteration-latest-iterations",
|
||||
kwargs={
|
||||
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
||||
"object_id": "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.data, [])
|
||||
|
||||
def test_iteration_includes_user_can_review(self):
|
||||
rule = LifecycleRule.objects.create(
|
||||
@@ -279,6 +291,11 @@ class TestReviewAPI(APITestCase):
|
||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||
self.reviewer_group.users.add(self.user)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
config = apps.get_app_config("authentik_tasks_schedules")
|
||||
config._on_startup_callback(None)
|
||||
|
||||
def test_create_review(self):
|
||||
rule = LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import datetime as dt
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.utils import timezone
|
||||
@@ -29,6 +30,11 @@ class TestLifecycleModels(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
config = apps.get_app_config("authentik_tasks_schedules")
|
||||
config._on_startup_callback(None)
|
||||
|
||||
def _get_request(self):
|
||||
return self.factory.get("/")
|
||||
|
||||
@@ -438,31 +444,6 @@ class TestLifecycleModels(TestCase):
|
||||
self.assertIn(app_one, objects)
|
||||
self.assertIn(app_two, objects)
|
||||
|
||||
def test_rule_type_excludes_objects_with_specific_rules(self):
|
||||
app_with_rule = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
app_without_rule = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(Application)
|
||||
|
||||
# Create a specific rule for app_with_rule
|
||||
LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
content_type=content_type,
|
||||
object_id=str(app_with_rule.pk),
|
||||
interval="days=30",
|
||||
)
|
||||
|
||||
# Create a type-level rule
|
||||
type_rule = LifecycleRule.objects.create(
|
||||
name=generate_id(),
|
||||
content_type=content_type,
|
||||
object_id=None,
|
||||
interval="days=60",
|
||||
)
|
||||
|
||||
objects = list(type_rule.get_objects())
|
||||
self.assertNotIn(app_with_rule, objects)
|
||||
self.assertIn(app_without_rule, objects)
|
||||
|
||||
def test_rule_type_apply_creates_iterations_for_all_objects(self):
|
||||
app_one = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
app_two = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
@@ -669,6 +650,73 @@ class TestLifecycleModels(TestCase):
|
||||
self.assertIn(explicit_reviewer, reviewers)
|
||||
self.assertIn(group_member, reviewers)
|
||||
|
||||
def test_multiple_rules_same_object_create_separate_iterations(self):
|
||||
"""Two rules targeting the same object each create their own iteration."""
|
||||
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
|
||||
rule_one = self._create_rule_for_object(obj, interval="days=30", grace_period="days=10")
|
||||
rule_two = self._create_rule_for_object(obj, interval="days=60", grace_period="days=20")
|
||||
|
||||
iterations = LifecycleIteration.objects.filter(
|
||||
content_type=content_type, object_id=str(obj.pk)
|
||||
)
|
||||
self.assertEqual(iterations.count(), 2)
|
||||
|
||||
iter_one = iterations.get(rule=rule_one)
|
||||
iter_two = iterations.get(rule=rule_two)
|
||||
self.assertEqual(iter_one.state, ReviewState.PENDING)
|
||||
self.assertEqual(iter_two.state, ReviewState.PENDING)
|
||||
self.assertNotEqual(iter_one.pk, iter_two.pk)
|
||||
|
||||
def test_multiple_rules_same_object_reviewed_independently(self):
|
||||
"""Reviewing one rule's iteration does not affect the other rule's iteration."""
|
||||
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
|
||||
reviewer = create_test_user()
|
||||
|
||||
rule_one = self._create_rule_for_object(obj, min_reviewers=1)
|
||||
rule_two = self._create_rule_for_object(obj, min_reviewers=1)
|
||||
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.users.add(reviewer)
|
||||
rule_one.reviewer_groups.add(group)
|
||||
rule_two.reviewer_groups.add(group)
|
||||
|
||||
iter_one = LifecycleIteration.objects.get(
|
||||
content_type=content_type, object_id=str(obj.pk), rule=rule_one
|
||||
)
|
||||
iter_two = LifecycleIteration.objects.get(
|
||||
content_type=content_type, object_id=str(obj.pk), rule=rule_two
|
||||
)
|
||||
|
||||
request = self._get_request()
|
||||
|
||||
# Review only rule_one's iteration
|
||||
Review.objects.create(iteration=iter_one, reviewer=reviewer)
|
||||
iter_one.on_review(request)
|
||||
|
||||
iter_one.refresh_from_db()
|
||||
iter_two.refresh_from_db()
|
||||
self.assertEqual(iter_one.state, ReviewState.REVIEWED)
|
||||
self.assertEqual(iter_two.state, ReviewState.PENDING)
|
||||
|
||||
def test_type_rule_and_object_rule_both_create_iterations(self):
|
||||
"""A type-level rule and an object-level rule both create iterations for the same object."""
|
||||
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
|
||||
object_rule = self._create_rule_for_object(obj, interval="days=30")
|
||||
type_rule = self._create_rule_for_type(Application, interval="days=60")
|
||||
|
||||
iterations = LifecycleIteration.objects.filter(
|
||||
content_type=content_type, object_id=str(obj.pk)
|
||||
)
|
||||
self.assertEqual(iterations.count(), 2)
|
||||
self.assertTrue(iterations.filter(rule=object_rule).exists())
|
||||
self.assertTrue(iterations.filter(rule=type_rule).exists())
|
||||
|
||||
|
||||
class TestLifecycleDateBoundaries(TestCase):
|
||||
"""Verify that start_of_day normalization ensures correct overdue/due
|
||||
@@ -679,6 +727,11 @@ class TestLifecycleDateBoundaries(TestCase):
|
||||
ensures that the boundary is always at midnight, so millisecond variations
|
||||
in task execution time do not affect results."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
config = apps.get_app_config("authentik_tasks_schedules")
|
||||
config._on_startup_callback(None)
|
||||
|
||||
def _create_rule_and_iteration(self, grace_period="days=1", interval="days=365"):
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
content_type = ContentType.objects.get_for_model(Application)
|
||||
|
||||
@@ -14,6 +14,7 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.providers.ws_federation",
|
||||
"authentik.enterprise.reports",
|
||||
"authentik.enterprise.stages.account_lockdown",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.mtls",
|
||||
"authentik.enterprise.stages.source",
|
||||
|
||||
141
authentik/enterprise/stages/account_lockdown/api.py
Normal file
141
authentik/enterprise/stages/account_lockdown/api.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Account Lockdown Stage API Views"""
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import PrimaryKeyRelatedField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
|
||||
from authentik.core.models import (
|
||||
User,
|
||||
)
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin, enterprise_action
|
||||
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||
from authentik.enterprise.stages.account_lockdown.stage import (
|
||||
can_lock_user,
|
||||
get_lockdown_target_users,
|
||||
)
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AccountLockdownStageSerializer(EnterpriseRequiredMixin, StageSerializer):
|
||||
"""AccountLockdownStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = AccountLockdownStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"deactivate_user",
|
||||
"set_unusable_password",
|
||||
"delete_sessions",
|
||||
"revoke_tokens",
|
||||
"self_service_completion_flow",
|
||||
]
|
||||
|
||||
|
||||
class AccountLockdownStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AccountLockdownStage Viewset"""
|
||||
|
||||
queryset = AccountLockdownStage.objects.all()
|
||||
serializer_class = AccountLockdownStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class UserAccountLockdownSerializer(PassiveSerializer):
|
||||
"""Choose the target account before starting the lockdown flow."""
|
||||
|
||||
user = PrimaryKeyRelatedField(
|
||||
queryset=get_lockdown_target_users(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text=_("User to lock. If omitted, locks the current user (self-service)."),
|
||||
)
|
||||
|
||||
|
||||
class UserAccountLockdownMixin:
|
||||
"""Enterprise account-lockdown API actions for UserViewSet."""
|
||||
|
||||
def _create_lockdown_flow_url(self, request: Request, user: User) -> str:
|
||||
"""Create a flow URL for account lockdown.
|
||||
|
||||
The request body selects the target before the flow starts. The API
|
||||
pre-plans the lockdown flow with the target as the pending user, so the
|
||||
account lockdown stage can use the normal flow context.
|
||||
"""
|
||||
flow = request._request.brand.flow_lockdown
|
||||
if flow is None:
|
||||
raise ValidationError({"non_field_errors": [_("No lockdown flow configured.")]})
|
||||
planner = FlowPlanner(flow)
|
||||
planner.use_cache = False
|
||||
try:
|
||||
plan = planner.plan(request._request, {PLAN_CONTEXT_PENDING_USER: user})
|
||||
except EmptyFlowException, FlowNonApplicableException:
|
||||
raise ValidationError(
|
||||
{"non_field_errors": [_("Lockdown flow is not applicable.")]}
|
||||
) from None
|
||||
return plan.to_redirect(request._request, flow).url
|
||||
|
||||
@extend_schema(
|
||||
description=_("Choose the target account, then return a flow link."),
|
||||
request=UserAccountLockdownSerializer,
|
||||
responses={
|
||||
"200": OpenApiResponse(
|
||||
response=LinkSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Lockdown flow URL",
|
||||
value={
|
||||
"link": "https://example.invalid/if/flow/default-account-lockdown/",
|
||||
},
|
||||
response_only=True,
|
||||
status_codes=["200"],
|
||||
)
|
||||
],
|
||||
),
|
||||
"400": OpenApiResponse(
|
||||
description=_("No lockdown flow configured or the flow is not applicable")
|
||||
),
|
||||
"403": OpenApiResponse(
|
||||
description=_("Permission denied (when targeting another user)")
|
||||
),
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["POST"],
|
||||
permission_classes=[IsAuthenticated],
|
||||
url_path="account_lockdown",
|
||||
)
|
||||
@validate(UserAccountLockdownSerializer)
|
||||
@enterprise_action
|
||||
def account_lockdown(self, request: Request, body: UserAccountLockdownSerializer) -> Response:
|
||||
"""Trigger account lockdown for a user.
|
||||
|
||||
If no user is specified, locks the current user (self-service).
|
||||
When targeting another user, admin permissions are required.
|
||||
|
||||
Returns a flow link for the frontend to follow. The flow is pre-planned
|
||||
with the target user as pending user for the lockdown stage.
|
||||
"""
|
||||
user = body.validated_data.get("user") or request.user
|
||||
|
||||
if not can_lock_user(request.user, user):
|
||||
LOGGER.debug("Permission denied for account lockdown", user=request.user)
|
||||
self.permission_denied(request)
|
||||
|
||||
flow_url = self._create_lockdown_flow_url(request, user)
|
||||
LOGGER.debug("Returning lockdown flow URL", flow_url=flow_url, user=user.username)
|
||||
return Response({"link": flow_url})
|
||||
12
authentik/enterprise/stages/account_lockdown/apps.py
Normal file
12
authentik/enterprise/stages/account_lockdown/apps.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""authentik account lockdown stage app config"""
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseStageAccountLockdownConfig(EnterpriseConfig):
|
||||
"""authentik account lockdown stage config"""
|
||||
|
||||
name = "authentik.enterprise.stages.account_lockdown"
|
||||
label = "authentik_stages_account_lockdown"
|
||||
verbose_name = "authentik Enterprise.Stages.Account Lockdown"
|
||||
default = True
|
||||
@@ -0,0 +1,74 @@
|
||||
# Generated by Django 5.2.13 on 2026-04-19 21:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0031_alter_flow_layout"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AccountLockdownStage",
|
||||
fields=[
|
||||
(
|
||||
"stage_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_flows.stage",
|
||||
),
|
||||
),
|
||||
(
|
||||
"deactivate_user",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Deactivate the user account (set is_active to False)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"set_unusable_password",
|
||||
models.BooleanField(
|
||||
default=True, help_text="Set an unusable password for the user"
|
||||
),
|
||||
),
|
||||
(
|
||||
"delete_sessions",
|
||||
models.BooleanField(
|
||||
default=True, help_text="Delete all active sessions for the user"
|
||||
),
|
||||
),
|
||||
(
|
||||
"revoke_tokens",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Revoke all tokens for the user (API, app password, recovery, verification, OAuth)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"self_service_completion_flow",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="account_lockdown_stages",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Account Lockdown Stage",
|
||||
"verbose_name_plural": "Account Lockdown Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage",),
|
||||
),
|
||||
]
|
||||
62
authentik/enterprise/stages/account_lockdown/models.py
Normal file
62
authentik/enterprise/stages/account_lockdown/models.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Account lockdown stage models"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import Stage
|
||||
|
||||
|
||||
class AccountLockdownStage(Stage):
|
||||
"""Lock down a target user account."""
|
||||
|
||||
deactivate_user = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("Deactivate the user account (set is_active to False)"),
|
||||
)
|
||||
set_unusable_password = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("Set an unusable password for the user"),
|
||||
)
|
||||
delete_sessions = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("Delete all active sessions for the user"),
|
||||
)
|
||||
revoke_tokens = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_(
|
||||
"Revoke all tokens for the user (API, app password, recovery, verification, OAuth)"
|
||||
),
|
||||
)
|
||||
self_service_completion_flow = models.ForeignKey(
|
||||
"authentik_flows.Flow",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="account_lockdown_stages",
|
||||
help_text=_(
|
||||
"Flow to redirect users to after self-service lockdown. "
|
||||
"This flow should not require authentication since the user's session is deleted."
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.enterprise.stages.account_lockdown.api import AccountLockdownStageSerializer
|
||||
|
||||
return AccountLockdownStageSerializer
|
||||
|
||||
@property
|
||||
def view(self) -> type[View]:
|
||||
from authentik.enterprise.stages.account_lockdown.stage import AccountLockdownStageView
|
||||
|
||||
return AccountLockdownStageView
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-stage-account-lockdown-form"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Account Lockdown Stage")
|
||||
verbose_name_plural = _("Account Lockdown Stages")
|
||||
345
authentik/enterprise/stages/account_lockdown/stage.py
Normal file
345
authentik/enterprise/stages/account_lockdown/stage.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""Account lockdown stage logic"""
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models import Model, QuerySet
|
||||
from django.db.models.query_utils import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dramatiq.actor import Actor
|
||||
from dramatiq.composition import group
|
||||
from dramatiq.results.errors import ResultTimeout
|
||||
|
||||
from authentik.core.models import (
|
||||
AuthenticatedSession,
|
||||
ExpiringModel,
|
||||
Session,
|
||||
Token,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||
from authentik.lib.sync.outgoing.signals import sync_outgoing_inhibit_dispatch
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
PLAN_CONTEXT_LOCKDOWN_REASON = "lockdown_reason"
|
||||
LOCKDOWN_EVENT_ACTION_ID = "account_lockdown"
|
||||
|
||||
TARGET_REQUIRED_MESSAGE = _("No target user specified for account lockdown")
|
||||
PERMISSION_DENIED_MESSAGE = _("You do not have permission to lock down this account.")
|
||||
ACCOUNT_LOCKDOWN_FAILED_MESSAGE = _("Account lockdown failed for this account.")
|
||||
SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE = _(
|
||||
"Self-service account lockdown requires a completion flow."
|
||||
)
|
||||
|
||||
|
||||
def get_lockdown_target_users() -> QuerySet[User]:
|
||||
"""Return users that can be targeted by account lockdown."""
|
||||
return User.objects.exclude_anonymous().exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
|
||||
|
||||
def _get_model_field(model: type[Model], field_name: str):
|
||||
"""Get a model field by name, if present."""
|
||||
try:
|
||||
return model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def _has_user_field(model: type[Model]) -> bool:
|
||||
"""Check if a model has a direct user foreign key."""
|
||||
field = _get_model_field(model, "user")
|
||||
return bool(field and getattr(field, "remote_field", None) and field.remote_field.model is User)
|
||||
|
||||
|
||||
def _has_authenticated_session_field(model: type[Model]) -> bool:
|
||||
"""Check if a model is linked to an authenticated session."""
|
||||
field = _get_model_field(model, "session")
|
||||
return bool(
|
||||
field
|
||||
and getattr(field, "remote_field", None)
|
||||
and field.remote_field.model is AuthenticatedSession
|
||||
)
|
||||
|
||||
|
||||
def _has_provider_field(model: type[Model]) -> bool:
|
||||
"""Check if a model is linked to a provider."""
|
||||
return _get_model_field(model, "provider") is not None
|
||||
|
||||
|
||||
def get_lockdown_token_models() -> tuple[type[Model], ...]:
|
||||
"""Return token, grant, and provider session models removed by account lockdown."""
|
||||
token_models: list[type[Model]] = []
|
||||
for model in apps.get_models():
|
||||
if model._meta.abstract or not issubclass(model, ExpiringModel):
|
||||
continue
|
||||
if model is Token:
|
||||
token_models.append(model)
|
||||
elif _has_user_field(model) and (
|
||||
_has_provider_field(model) or _has_authenticated_session_field(model)
|
||||
):
|
||||
token_models.append(model)
|
||||
elif _has_authenticated_session_field(model):
|
||||
token_models.append(model)
|
||||
return tuple(token_models)
|
||||
|
||||
|
||||
def get_lockdown_token_queryset(model: type[Model], user: User) -> QuerySet:
|
||||
"""Return account lockdown artifacts for a model and user."""
|
||||
manager = model.objects.including_expired()
|
||||
if _has_user_field(model):
|
||||
return manager.filter(user=user)
|
||||
return manager.filter(session__user=user)
|
||||
|
||||
|
||||
def can_lock_user(actor, user: User) -> bool:
|
||||
"""Check whether the actor may lock the target user."""
|
||||
if not actor.is_authenticated:
|
||||
return False
|
||||
if user.pk == actor.pk:
|
||||
return True
|
||||
return actor.has_perm("authentik_core.change_user", user)
|
||||
|
||||
|
||||
def get_outgoing_sync_tasks() -> tuple[tuple[type[OutgoingSyncProvider], Actor], ...]:
|
||||
"""Return outgoing sync provider types and their direct sync tasks."""
|
||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync_direct
|
||||
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
||||
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync_direct
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_sync_direct
|
||||
|
||||
return (
|
||||
(SCIMProvider, scim_sync_direct),
|
||||
(GoogleWorkspaceProvider, google_workspace_sync_direct),
|
||||
(MicrosoftEntraProvider, microsoft_entra_sync_direct),
|
||||
)
|
||||
|
||||
|
||||
class AccountLockdownStageView(StageView):
|
||||
"""Execute account lockdown actions on the target user."""
|
||||
|
||||
def is_self_service(self, request: HttpRequest, user: User) -> bool:
|
||||
"""Check whether the currently authenticated user is locking their own account."""
|
||||
return request.user.is_authenticated and user.pk == request.user.pk
|
||||
|
||||
def get_reason(self) -> str:
|
||||
"""Get the lockdown reason from the plan context.
|
||||
|
||||
Priority:
|
||||
1. prompt_data[PLAN_CONTEXT_LOCKDOWN_REASON]
|
||||
2. PLAN_CONTEXT_LOCKDOWN_REASON (explicitly set)
|
||||
3. Empty string as fallback
|
||||
"""
|
||||
prompt_data = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||
if PLAN_CONTEXT_LOCKDOWN_REASON in prompt_data:
|
||||
return prompt_data[PLAN_CONTEXT_LOCKDOWN_REASON]
|
||||
return self.executor.plan.context.get(PLAN_CONTEXT_LOCKDOWN_REASON, "")
|
||||
|
||||
def _apply_lockdown_actions(self, stage: AccountLockdownStage, user: User) -> None:
|
||||
"""Apply the configured account changes to the target user."""
|
||||
if stage.deactivate_user:
|
||||
user.is_active = False
|
||||
if stage.set_unusable_password:
|
||||
user.set_unusable_password()
|
||||
if stage.deactivate_user:
|
||||
with sync_outgoing_inhibit_dispatch():
|
||||
user.save()
|
||||
return
|
||||
user.save()
|
||||
|
||||
def _sync_deactivated_user_to_outgoing_providers(self, user: User) -> None:
|
||||
"""Synchronize a deactivated user to outgoing sync providers."""
|
||||
messages = []
|
||||
wait_timeout = 0
|
||||
model = class_to_path(User)
|
||||
provider_filter = Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||
|
||||
for provider_model, task_sync_direct in get_outgoing_sync_tasks():
|
||||
for provider in provider_model.objects.filter(provider_filter):
|
||||
time_limit = int(
|
||||
timedelta_from_string(provider.sync_page_timeout).total_seconds() * 1000
|
||||
)
|
||||
messages.append(
|
||||
task_sync_direct.message_with_options(
|
||||
args=(model, user.pk, provider.pk),
|
||||
rel_obj=provider,
|
||||
time_limit=time_limit,
|
||||
uid=f"{provider.name}:user:{user.pk}:direct",
|
||||
)
|
||||
)
|
||||
wait_timeout += time_limit
|
||||
|
||||
if not messages:
|
||||
return
|
||||
try:
|
||||
group(messages).run().wait(timeout=wait_timeout)
|
||||
except ResultTimeout:
|
||||
self.logger.warning(
|
||||
"Timed out waiting for outgoing sync tasks; tasks remain queued",
|
||||
user=user.username,
|
||||
timeout=wait_timeout,
|
||||
)
|
||||
|
||||
def _get_lockdown_artifact_querysets(
|
||||
self, stage: AccountLockdownStage, user: User
|
||||
) -> tuple[QuerySet, ...]:
|
||||
"""Return the configured sessions and tokens targeted by lockdown."""
|
||||
querysets: list[QuerySet] = []
|
||||
if stage.delete_sessions:
|
||||
querysets.append(Session.objects.filter(authenticatedsession__user=user))
|
||||
if stage.revoke_tokens:
|
||||
querysets.extend(
|
||||
get_lockdown_token_queryset(model, user) for model in get_lockdown_token_models()
|
||||
)
|
||||
return tuple(querysets)
|
||||
|
||||
def _delete_lockdown_artifacts(self, stage: AccountLockdownStage, user: User) -> None:
|
||||
"""Delete sessions and tokens selected by the lockdown configuration."""
|
||||
for queryset in self._get_lockdown_artifact_querysets(stage, user):
|
||||
queryset.delete()
|
||||
|
||||
def _has_lockdown_artifacts(self, stage: AccountLockdownStage, user: User) -> bool:
|
||||
"""Check whether there are still sessions or tokens to remove."""
|
||||
return any(
|
||||
queryset.exists() for queryset in self._get_lockdown_artifact_querysets(stage, user)
|
||||
)
|
||||
|
||||
def _emit_lockdown_event(self, request: HttpRequest, user: User, reason: str) -> None:
|
||||
"""Emit the audit event for a completed lockdown."""
|
||||
# Emit the audit event after the transaction commits. If event creation
|
||||
# fails here, dispatch() would otherwise treat the whole lockdown as
|
||||
# failed even though the account changes have already been committed.
|
||||
try:
|
||||
Event.new(
|
||||
EventAction.USER_WRITE,
|
||||
action_id=LOCKDOWN_EVENT_ACTION_ID,
|
||||
reason=reason,
|
||||
affected_user=user.username,
|
||||
).from_http(request)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Event emission should not make the lockdown itself fail.
|
||||
self.logger.warning(
|
||||
"Failed to emit account lockdown event",
|
||||
user=user.username,
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
def _lockdown_user(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
stage: AccountLockdownStage,
|
||||
user: User,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Execute lockdown actions on a single user."""
|
||||
with atomic():
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self._apply_lockdown_actions(stage, user)
|
||||
self._delete_lockdown_artifacts(stage, user)
|
||||
|
||||
# These additional checks/deletes are done to prevent a timing attack that creates tokens
|
||||
# with a compromised token that is simultaneously being deleted.
|
||||
while self._has_lockdown_artifacts(stage, user):
|
||||
with atomic():
|
||||
self._delete_lockdown_artifacts(stage, user)
|
||||
|
||||
if stage.deactivate_user:
|
||||
try:
|
||||
self._sync_deactivated_user_to_outgoing_providers(user)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Local lockdown has already committed. Provider sync failures
|
||||
# must not reopen access or mark the lockdown itself as failed.
|
||||
self.logger.warning(
|
||||
"Failed to sync account lockdown deactivation to outgoing providers",
|
||||
user=user.username,
|
||||
exc=exc,
|
||||
)
|
||||
self._emit_lockdown_event(request, user, reason)
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Execute account lockdown actions."""
|
||||
self.request = request
|
||||
stage: AccountLockdownStage = self.executor.current_stage
|
||||
|
||||
pending_user = self.get_pending_user()
|
||||
if not pending_user.is_authenticated:
|
||||
self.logger.warning("No target user found for account lockdown")
|
||||
return self.executor.stage_invalid(TARGET_REQUIRED_MESSAGE)
|
||||
user = get_lockdown_target_users().filter(pk=pending_user.pk).first()
|
||||
if user is None:
|
||||
self.logger.warning("Target user is not eligible for account lockdown")
|
||||
return self.executor.stage_invalid(TARGET_REQUIRED_MESSAGE)
|
||||
if not can_lock_user(request.user, user):
|
||||
self.logger.warning(
|
||||
"Permission denied for account lockdown",
|
||||
actor=getattr(request.user, "username", None),
|
||||
target=user.username,
|
||||
)
|
||||
return self.executor.stage_invalid(PERMISSION_DENIED_MESSAGE)
|
||||
|
||||
reason = self.get_reason()
|
||||
self_service = self.is_self_service(request, user)
|
||||
if self_service and stage.delete_sessions and not stage.self_service_completion_flow:
|
||||
self.logger.warning("No completion flow configured for self-service account lockdown")
|
||||
return self.executor.stage_invalid(SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE)
|
||||
|
||||
self.logger.info(
|
||||
"Executing account lockdown",
|
||||
user=user.username,
|
||||
reason=reason,
|
||||
self_service=self_service,
|
||||
deactivate_user=stage.deactivate_user,
|
||||
set_unusable_password=stage.set_unusable_password,
|
||||
delete_sessions=stage.delete_sessions,
|
||||
revoke_tokens=stage.revoke_tokens,
|
||||
)
|
||||
|
||||
try:
|
||||
self._lockdown_user(request, stage, user, reason)
|
||||
self.logger.info("Account lockdown completed", user=user.username)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Convert unexpected lockdown errors to a flow-stage failure instead
|
||||
# of leaking an exception through the flow executor.
|
||||
self.logger.warning("Account lockdown failed", user=user.username, exc=exc)
|
||||
return self.executor.stage_invalid(ACCOUNT_LOCKDOWN_FAILED_MESSAGE)
|
||||
|
||||
if self_service:
|
||||
if stage.delete_sessions:
|
||||
return self._self_service_completion_response(request)
|
||||
return self.executor.stage_ok()
|
||||
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def _self_service_completion_response(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Redirect to completion flow after self-service lockdown.
|
||||
|
||||
Since all sessions are deleted, the user cannot continue in the flow.
|
||||
Redirect them to an unauthenticated completion flow that shows the
|
||||
lockdown message.
|
||||
|
||||
We use a direct HTTP redirect instead of a challenge because the
|
||||
flow executor's challenge handling may try to access the session
|
||||
which we just deleted.
|
||||
"""
|
||||
stage: AccountLockdownStage = self.executor.current_stage
|
||||
completion_flow = stage.self_service_completion_flow
|
||||
if completion_flow:
|
||||
# Flush the current request's session to prevent Django's session
|
||||
# middleware from trying to save a deleted session
|
||||
if hasattr(request, "session"):
|
||||
request.session.flush()
|
||||
redirect_to = reverse(
|
||||
"authentik_core:if-flow",
|
||||
kwargs={"flow_slug": completion_flow.slug},
|
||||
)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
return self.executor.stage_invalid(SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE)
|
||||
148
authentik/enterprise/stages/account_lockdown/tests/test_api.py
Normal file
148
authentik/enterprise/stages/account_lockdown/tests/test_api.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Test Users Account Lockdown API"""
|
||||
|
||||
from json import loads
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import (
|
||||
create_test_brand,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
# Patch for enterprise license check
|
||||
patch_license = patch(
|
||||
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
|
||||
MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
|
||||
@patch_license
|
||||
class AccountLockdownAPITestCase(APITestCase):
|
||||
"""Shared helpers for account lockdown API tests."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.lockdown_flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||
self.lockdown_stage = AccountLockdownStage.objects.create(name=generate_id())
|
||||
FlowStageBinding.objects.create(
|
||||
target=self.lockdown_flow,
|
||||
stage=self.lockdown_stage,
|
||||
order=0,
|
||||
)
|
||||
self.brand = create_test_brand()
|
||||
self.brand.flow_lockdown = self.lockdown_flow
|
||||
self.brand.save()
|
||||
|
||||
def create_user_with_email(self):
|
||||
"""Create a regular user with a unique email address."""
|
||||
user = create_test_user()
|
||||
user.email = f"{generate_id()}@test.com"
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def assert_redirect_targets(self, response, user):
|
||||
"""Assert that a response contains a pre-planned lockdown flow link for a user."""
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertIn(self.lockdown_flow.slug, body["link"])
|
||||
self.assertEqual(urlparse(body["link"]).query, "")
|
||||
plan = self.client.session[SESSION_KEY_PLAN]
|
||||
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER].pk, user.pk)
|
||||
|
||||
def assert_no_flow_configured(self, response):
|
||||
"""Assert that the API reports a missing lockdown flow."""
|
||||
self.assertEqual(response.status_code, 400)
|
||||
body = loads(response.content)
|
||||
self.assertIn("No lockdown flow configured", body["non_field_errors"][0])
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestUsersAccountLockdownAPI(AccountLockdownAPITestCase):
|
||||
"""Test Users Account Lockdown API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.actor = create_test_user()
|
||||
self.user = self.create_user_with_email()
|
||||
|
||||
def test_account_lockdown_with_change_user_returns_redirect(self):
|
||||
"""Test that account lockdown allows users with change_user permission."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.user)
|
||||
self.client.force_login(self.actor)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={"user": self.user.pk},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assert_redirect_targets(response, self.user)
|
||||
|
||||
def test_account_lockdown_no_flow_configured(self):
|
||||
"""Test account lockdown when no flow is configured"""
|
||||
self.brand.flow_lockdown = None
|
||||
self.brand.save()
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.user)
|
||||
self.client.force_login(self.actor)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={"user": self.user.pk},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assert_no_flow_configured(response)
|
||||
|
||||
def test_account_lockdown_unauthenticated(self):
|
||||
"""Test account lockdown requires authentication"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={"user": self.user.pk},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_account_lockdown_without_change_user_denied(self):
|
||||
"""Test account lockdown denies users without change_user permission."""
|
||||
self.client.force_login(self.actor)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={"user": self.user.pk},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_account_lockdown_self_returns_redirect(self):
|
||||
"""Test successful self-service account lockdown returns a direct redirect."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assert_redirect_targets(response, self.user)
|
||||
|
||||
def test_account_lockdown_self_target_without_change_user_returns_redirect(self):
|
||||
"""Test self-service does not require change_user permission."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-account-lockdown"),
|
||||
data={"user": self.user.pk},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assert_redirect_targets(response, self.user)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Tests for the packaged account-lockdown blueprint."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.blueprints.v1.tasks import blueprints_find, check_blueprint_v1_file
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.flows.models import Flow
|
||||
|
||||
BLUEPRINT_PATH = "example/flow-default-account-lockdown.yaml"
|
||||
|
||||
|
||||
class TestAccountLockdownBlueprint(TransactionTestCase):
|
||||
"""Test the packaged account-lockdown blueprint behavior."""
|
||||
|
||||
def test_blueprint_is_not_auto_instantiated(self):
|
||||
"""Test the packaged blueprint is opt-in and skipped by discovery."""
|
||||
BlueprintInstance.objects.filter(path=BLUEPRINT_PATH).delete()
|
||||
blueprint = next(item for item in blueprints_find() if item.path == BLUEPRINT_PATH)
|
||||
|
||||
check_blueprint_v1_file(blueprint)
|
||||
|
||||
self.assertFalse(BlueprintInstance.objects.filter(path=BLUEPRINT_PATH).exists())
|
||||
|
||||
def test_blueprint_requires_licensed_context(self):
|
||||
"""Test manual import only creates flows when enterprise is licensed."""
|
||||
content = BlueprintInstance(path=BLUEPRINT_PATH).retrieve()
|
||||
license_key = LicenseKey("test", 253402300799, "Test license", 1000, 1000)
|
||||
|
||||
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||
importer = Importer.from_string(content, {"goauthentik.io/enterprise/licensed": False})
|
||||
valid, logs = importer.validate()
|
||||
self.assertTrue(valid, logs)
|
||||
self.assertTrue(importer.apply())
|
||||
self.assertFalse(Flow.objects.filter(slug="default-account-lockdown").exists())
|
||||
self.assertFalse(Flow.objects.filter(slug="default-account-lockdown-complete").exists())
|
||||
|
||||
importer = Importer.from_string(content, {"goauthentik.io/enterprise/licensed": True})
|
||||
valid, logs = importer.validate()
|
||||
self.assertTrue(valid, logs)
|
||||
self.assertTrue(importer.apply())
|
||||
self.assertTrue(Flow.objects.filter(slug="default-account-lockdown").exists())
|
||||
self.assertTrue(Flow.objects.filter(slug="default-account-lockdown-complete").exists())
|
||||
627
authentik/enterprise/stages/account_lockdown/tests/test_stage.py
Normal file
627
authentik/enterprise/stages/account_lockdown/tests/test_stage.py
Normal file
@@ -0,0 +1,627 @@
|
||||
"""Account lockdown stage tests"""
|
||||
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from threading import Event as ThreadEvent
|
||||
from threading import Thread
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.db import connection
|
||||
from django.http import HttpResponse
|
||||
from django.test import TransactionTestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from dramatiq.results.errors import ResultTimeout
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, Session, Token, TokenIntents
|
||||
from authentik.core.tests.utils import (
|
||||
RequestFactory,
|
||||
create_test_admin_user,
|
||||
create_test_cert,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||
from authentik.enterprise.stages.account_lockdown.stage import (
|
||||
LOCKDOWN_EVENT_ACTION_ID,
|
||||
PLAN_CONTEXT_LOCKDOWN_REASON,
|
||||
AccountLockdownStageView,
|
||||
can_lock_user,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
AuthorizationCode,
|
||||
DeviceToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.saml.models import SAMLProvider, SAMLSession
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
patch_enterprise_enabled = patch(
|
||||
"authentik.enterprise.apps.AuthentikEnterpriseConfig.check_enabled",
|
||||
return_value=True,
|
||||
)
|
||||
|
||||
|
||||
class AccountLockdownStageTestMixin:
|
||||
"""Shared setup helpers for account lockdown stage tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.patch_enterprise_enabled = patch_enterprise_enabled.start()
|
||||
cls.patch_event_dispatch = patch("authentik.events.tasks.event_trigger_dispatch.send")
|
||||
cls.patch_event_dispatch.start()
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.patch_event_dispatch.stop()
|
||||
patch_enterprise_enabled.stop()
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = create_test_admin_user()
|
||||
self.target_user = create_test_admin_user()
|
||||
self.flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||
self.stage = AccountLockdownStage.objects.create(
|
||||
name="lockdown",
|
||||
)
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0)
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def make_stage_view(self, plan: FlowPlan):
|
||||
def _stage_ok():
|
||||
return HttpResponse(status=204)
|
||||
|
||||
def _stage_invalid(_error_message=None):
|
||||
return HttpResponse(status=400)
|
||||
|
||||
return AccountLockdownStageView(
|
||||
SimpleNamespace(
|
||||
plan=plan,
|
||||
current_stage=self.stage,
|
||||
current_binding=self.binding,
|
||||
flow=self.flow,
|
||||
stage_ok=_stage_ok,
|
||||
stage_invalid=_stage_invalid,
|
||||
)
|
||||
)
|
||||
|
||||
def make_request(self, *, user=None, query=None):
|
||||
return self.request_factory.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
query_params=query or {},
|
||||
user=user,
|
||||
)
|
||||
|
||||
def get_lockdown_event(self):
|
||||
"""Return the account-lockdown user-write event."""
|
||||
return Event.objects.filter(
|
||||
action=EventAction.USER_WRITE,
|
||||
context__action_id=LOCKDOWN_EVENT_ACTION_ID,
|
||||
).first()
|
||||
|
||||
|
||||
class TestAccountLockdownStage(AccountLockdownStageTestMixin, FlowTestCase):
|
||||
"""Account lockdown stage tests"""
|
||||
|
||||
def test_lockdown_no_target(self):
|
||||
"""Test lockdown stage with no pending user fails"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
|
||||
response = view.dispatch(self.make_request())
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_lockdown_with_pending_user(self):
|
||||
"""Test lockdown stage with a pending target user."""
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_LOCKDOWN_REASON] = "Security incident"
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.user)
|
||||
|
||||
self.assertTrue(can_lock_user(request.user, self.target_user))
|
||||
response = view.dispatch(request)
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
self.assertFalse(self.target_user.has_usable_password())
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
# Check event was created
|
||||
event = self.get_lockdown_event()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event.context["action_id"], LOCKDOWN_EVENT_ACTION_ID)
|
||||
self.assertEqual(event.context["reason"], "Security incident")
|
||||
self.assertEqual(event.context["affected_user"], self.target_user.username)
|
||||
|
||||
def test_lockdown_with_pending_user_reason(self):
|
||||
"""Test lockdown stage with a pending target and explicit reason."""
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_LOCKDOWN_REASON] = "Compromised account"
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.user)
|
||||
|
||||
self.assertTrue(can_lock_user(request.user, self.target_user))
|
||||
response = view.dispatch(request)
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_lockdown_reason_from_prompt(self):
|
||||
"""Test lockdown stage reads the reason from prompt data."""
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
PLAN_CONTEXT_LOCKDOWN_REASON: "User requested lockdown",
|
||||
}
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.user)
|
||||
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||
|
||||
event = self.get_lockdown_event()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event.context["reason"], "User requested lockdown")
|
||||
|
||||
def test_lockdown_event_failure_does_not_fail_self_service(self):
|
||||
"""Test lockdown still succeeds when event emission fails."""
|
||||
self.stage.delete_sessions = False
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.target_user)
|
||||
|
||||
original_event_new = Event.new
|
||||
|
||||
def _event_new_side_effect(action, *args, **kwargs):
|
||||
if (
|
||||
action == EventAction.USER_WRITE
|
||||
and kwargs.get("action_id") == LOCKDOWN_EVENT_ACTION_ID
|
||||
):
|
||||
raise RuntimeError("simulated event failure")
|
||||
return original_event_new(action, *args, **kwargs)
|
||||
|
||||
with patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.Event.new",
|
||||
side_effect=_event_new_side_effect,
|
||||
):
|
||||
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
|
||||
def test_dispatch_records_success_when_event_emission_fails(self):
|
||||
"""Test dispatch still completes if event emission fails."""
|
||||
self.stage.delete_sessions = False
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(
|
||||
user=self.target_user,
|
||||
)
|
||||
|
||||
original_event_new = Event.new
|
||||
|
||||
def _event_new_side_effect(action, *args, **kwargs):
|
||||
if (
|
||||
action == EventAction.USER_WRITE
|
||||
and kwargs.get("action_id") == LOCKDOWN_EVENT_ACTION_ID
|
||||
):
|
||||
raise RuntimeError("simulated event failure")
|
||||
return original_event_new(action, *args, **kwargs)
|
||||
|
||||
with patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.Event.new",
|
||||
side_effect=_event_new_side_effect,
|
||||
):
|
||||
response = view.dispatch(request)
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_lockdown_self_service_redirects_to_completion_flow(self):
|
||||
"""Test self-service lockdown redirects to completion flow when sessions are deleted."""
|
||||
completion_flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||
self.stage.self_service_completion_flow = completion_flow
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.target_user)
|
||||
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||
response = view._self_service_completion_response(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
response.url,
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": completion_flow.slug}),
|
||||
)
|
||||
|
||||
def test_lockdown_self_service_requires_completion_flow(self):
|
||||
"""Test self-service lockdown fails before deleting sessions without a completion flow."""
|
||||
self.stage.self_service_completion_flow = None
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=self.target_user)
|
||||
|
||||
response = view.dispatch(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertTrue(self.target_user.is_active)
|
||||
|
||||
def test_lockdown_denies_other_user_without_permission(self):
|
||||
"""Test lockdown stage rejects non-self requests without change_user permission."""
|
||||
actor = create_test_user()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||
view = self.make_stage_view(plan)
|
||||
request = self.make_request(user=actor)
|
||||
|
||||
self.assertFalse(can_lock_user(request.user, self.target_user))
|
||||
response = view.dispatch(request)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_lockdown_revokes_tokens(self):
|
||||
"""Test lockdown stage revokes tokens"""
|
||||
Token.objects.create(
|
||||
user=self.target_user,
|
||||
identifier="test-token",
|
||||
intent=TokenIntents.INTENT_API,
|
||||
key=generate_id(),
|
||||
expiring=False,
|
||||
)
|
||||
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 1)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 0)
|
||||
|
||||
def test_lockdown_revokes_provider_tokens(self):
|
||||
"""Test lockdown stage revokes provider tokens and sessions."""
|
||||
oauth_provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback")
|
||||
],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
saml_provider = SAMLProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
acs_url="https://sp.example.com/acs",
|
||||
issuer_override="https://idp.example.com",
|
||||
)
|
||||
session = Session.objects.create(
|
||||
session_key=generate_id(),
|
||||
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||
last_ip="127.0.0.1",
|
||||
)
|
||||
auth_session = AuthenticatedSession.objects.create(
|
||||
session=session,
|
||||
user=self.target_user,
|
||||
)
|
||||
grant_kwargs = {
|
||||
"provider": oauth_provider,
|
||||
"user": self.target_user,
|
||||
"auth_time": timezone.now(),
|
||||
"_scope": "openid profile",
|
||||
"expiring": False,
|
||||
}
|
||||
token_kwargs = grant_kwargs | {"_id_token": json.dumps(asdict(IDToken("foo", "bar")))}
|
||||
AuthorizationCode.objects.create(
|
||||
code=generate_id(),
|
||||
session=auth_session,
|
||||
**grant_kwargs,
|
||||
)
|
||||
AccessToken.objects.create(
|
||||
token=generate_id(),
|
||||
session=auth_session,
|
||||
**token_kwargs,
|
||||
)
|
||||
RefreshToken.objects.create(
|
||||
token=generate_id(),
|
||||
session=auth_session,
|
||||
**token_kwargs,
|
||||
)
|
||||
DeviceToken.objects.create(
|
||||
provider=oauth_provider,
|
||||
user=self.target_user,
|
||||
session=auth_session,
|
||||
_scope="openid profile",
|
||||
expiring=False,
|
||||
)
|
||||
SAMLSession.objects.create(
|
||||
provider=saml_provider,
|
||||
user=self.target_user,
|
||||
session=auth_session,
|
||||
session_index=generate_id(),
|
||||
name_id=self.target_user.email,
|
||||
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||
expiring=True,
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
self.assertEqual(AuthorizationCode.objects.filter(user=self.target_user).count(), 0)
|
||||
self.assertEqual(AccessToken.objects.filter(user=self.target_user).count(), 0)
|
||||
self.assertEqual(RefreshToken.objects.filter(user=self.target_user).count(), 0)
|
||||
self.assertEqual(DeviceToken.objects.filter(user=self.target_user).count(), 0)
|
||||
self.assertEqual(SAMLSession.objects.filter(user=self.target_user).count(), 0)
|
||||
|
||||
def test_lockdown_selective_actions(self):
|
||||
"""Test lockdown stage with selective actions"""
|
||||
self.stage.deactivate_user = True
|
||||
self.stage.set_unusable_password = False
|
||||
self.stage.delete_sessions = False
|
||||
self.stage.revoke_tokens = False
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.set_password("testpassword")
|
||||
self.target_user.save()
|
||||
|
||||
Token.objects.create(
|
||||
user=self.target_user,
|
||||
identifier="test-token",
|
||||
intent=TokenIntents.INTENT_API,
|
||||
key=generate_id(),
|
||||
expiring=False,
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
# User should be deactivated
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
# Password should still be usable
|
||||
self.assertTrue(self.target_user.has_usable_password())
|
||||
# Token should still exist
|
||||
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 1)
|
||||
|
||||
def test_lockdown_no_actions(self):
|
||||
"""Test lockdown stage with all actions disabled"""
|
||||
self.stage.deactivate_user = False
|
||||
self.stage.set_unusable_password = False
|
||||
self.stage.delete_sessions = False
|
||||
self.stage.revoke_tokens = False
|
||||
self.stage.save()
|
||||
|
||||
self.target_user.is_active = True
|
||||
self.target_user.set_password("testpassword")
|
||||
self.target_user.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
# User should still be active
|
||||
self.assertTrue(self.target_user.is_active)
|
||||
# Password should still be usable
|
||||
self.assertTrue(self.target_user.has_usable_password())
|
||||
# Event should still be created
|
||||
event = self.get_lockdown_event()
|
||||
self.assertIsNotNone(event)
|
||||
|
||||
def test_lockdown_deactivation_inhibits_signal_dispatch_until_after_commit(self):
|
||||
"""Test lockdown queues explicit outgoing syncs after the deactivation transaction."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.sync_outgoing_inhibit_dispatch"
|
||||
) as inhibit,
|
||||
patch.object(view, "_sync_deactivated_user_to_outgoing_providers") as sync_outgoing,
|
||||
):
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
inhibit.assert_called_once()
|
||||
sync_outgoing.assert_called_once()
|
||||
synced_user = sync_outgoing.call_args.args[0]
|
||||
self.assertEqual(synced_user.pk, self.target_user.pk)
|
||||
self.assertFalse(synced_user.is_active)
|
||||
|
||||
def test_lockdown_waits_for_direct_outgoing_provider_syncs(self):
|
||||
"""Test direct outgoing sync tasks are enqueued and waited on."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
provider = SimpleNamespace(name="outgoing", pk=1, sync_page_timeout="seconds=5")
|
||||
task_sync_direct = MagicMock()
|
||||
task_sync_direct.message_with_options.return_value = "direct-message"
|
||||
provider_model = SimpleNamespace(
|
||||
objects=SimpleNamespace(filter=MagicMock(return_value=[provider]))
|
||||
)
|
||||
task_group = MagicMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.get_outgoing_sync_tasks",
|
||||
return_value=((provider_model, task_sync_direct),),
|
||||
),
|
||||
patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.group",
|
||||
return_value=task_group,
|
||||
) as task_group_cls,
|
||||
):
|
||||
view._sync_deactivated_user_to_outgoing_providers(self.target_user)
|
||||
|
||||
task_sync_direct.message_with_options.assert_called_once_with(
|
||||
args=(class_to_path(type(self.target_user)), self.target_user.pk, provider.pk),
|
||||
rel_obj=provider,
|
||||
time_limit=5000,
|
||||
uid=f"{provider.name}:user:{self.target_user.pk}:direct",
|
||||
)
|
||||
task_group_cls.assert_called_once_with(["direct-message"])
|
||||
task_group.run.return_value.wait.assert_called_once_with(timeout=5000)
|
||||
|
||||
def test_lockdown_outgoing_provider_sync_timeout_leaves_tasks_running(self):
|
||||
"""Test timeout while waiting for direct outgoing syncs does not fail lockdown."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
provider = SimpleNamespace(name="outgoing", pk=1, sync_page_timeout="seconds=5")
|
||||
task_sync_direct = MagicMock()
|
||||
task_sync_direct.message_with_options.return_value = "direct-message"
|
||||
provider_model = SimpleNamespace(
|
||||
objects=SimpleNamespace(filter=MagicMock(return_value=[provider]))
|
||||
)
|
||||
task_group = MagicMock()
|
||||
task_group.run.return_value.wait.side_effect = ResultTimeout("timed out")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.get_outgoing_sync_tasks",
|
||||
return_value=((provider_model, task_sync_direct),),
|
||||
),
|
||||
patch(
|
||||
"authentik.enterprise.stages.account_lockdown.stage.group",
|
||||
return_value=task_group,
|
||||
),
|
||||
):
|
||||
view._sync_deactivated_user_to_outgoing_providers(self.target_user)
|
||||
|
||||
task_group.run.assert_called_once_with()
|
||||
task_group.run.return_value.wait.assert_called_once_with(timeout=5000)
|
||||
|
||||
def test_lockdown_outgoing_provider_sync_failure_does_not_fail_lockdown(self):
|
||||
"""Test completed local lockdown still emits an event if outgoing sync fails."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
|
||||
with patch.object(
|
||||
view,
|
||||
"_sync_deactivated_user_to_outgoing_providers",
|
||||
side_effect=ValueError("sync failed"),
|
||||
):
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
|
||||
self.target_user.refresh_from_db()
|
||||
self.assertFalse(self.target_user.is_active)
|
||||
event = self.get_lockdown_event()
|
||||
self.assertIsNotNone(event)
|
||||
|
||||
|
||||
class TestAccountLockdownStageConcurrency(AccountLockdownStageTestMixin, TransactionTestCase):
|
||||
"""Account lockdown concurrency tests."""
|
||||
|
||||
def test_lockdown_retries_when_another_transaction_recreates_a_token(self):
|
||||
"""Lockdown should remove a token recreated before the retry check runs."""
|
||||
Token.objects.create(
|
||||
user=self.target_user,
|
||||
identifier=f"initial-token-{generate_id()}",
|
||||
intent=TokenIntents.INTENT_API,
|
||||
key=generate_id(),
|
||||
expiring=False,
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
view = self.make_stage_view(plan)
|
||||
original_has_artifacts = view._has_lockdown_artifacts
|
||||
target_user = self.target_user
|
||||
thread_ready = ThreadEvent()
|
||||
start_create = ThreadEvent()
|
||||
thread_done = ThreadEvent()
|
||||
thread_errors = []
|
||||
|
||||
class TokenCreatorThread(Thread):
|
||||
__test__ = False
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
thread_ready.set()
|
||||
if not start_create.wait(timeout=5):
|
||||
thread_errors.append("timed out waiting to recreate token")
|
||||
return
|
||||
Token.objects.create(
|
||||
user=target_user,
|
||||
identifier=f"concurrent-token-{generate_id()}",
|
||||
intent=TokenIntents.INTENT_API,
|
||||
key=generate_id(),
|
||||
expiring=False,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
thread_errors.append(exc)
|
||||
finally:
|
||||
thread_done.set()
|
||||
connection.close()
|
||||
|
||||
def has_artifacts_after_concurrent_create(stage, user):
|
||||
if not start_create.is_set():
|
||||
start_create.set()
|
||||
self.assertTrue(
|
||||
thread_done.wait(timeout=30),
|
||||
(
|
||||
"Concurrent token creation did not complete "
|
||||
f"before retry check: {thread_errors}"
|
||||
),
|
||||
)
|
||||
return original_has_artifacts(stage, user)
|
||||
|
||||
creator = TokenCreatorThread()
|
||||
with patch.object(
|
||||
view, "_has_lockdown_artifacts", side_effect=has_artifacts_after_concurrent_create
|
||||
):
|
||||
creator.start()
|
||||
self.assertTrue(
|
||||
thread_ready.wait(timeout=5),
|
||||
"Concurrent token creation thread did not start",
|
||||
)
|
||||
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||
creator.join()
|
||||
|
||||
self.assertEqual(thread_errors, [])
|
||||
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 0)
|
||||
5
authentik/enterprise/stages/account_lockdown/urls.py
Normal file
5
authentik/enterprise/stages/account_lockdown/urls.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.stages.account_lockdown.api import AccountLockdownStageViewSet
|
||||
|
||||
api_urlpatterns = [("stages/account_lockdown", AccountLockdownStageViewSet)]
|
||||
@@ -172,6 +172,7 @@ SPECTACULAR_SETTINGS = {
|
||||
},
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"AppEnum": "authentik.lib.api.Apps",
|
||||
"AuthenticationEnum": "authentik.flows.models.FlowAuthenticationRequirement",
|
||||
"ConsentModeEnum": "authentik.stages.consent.models.ConsentMode",
|
||||
"CountryCodeEnum": "django_countries.countries",
|
||||
"DeviceClassesEnum": "authentik.stages.authenticator_validate.models.DeviceClasses",
|
||||
|
||||
64
authentik/stages/prompt/migrations/0012_alter_prompt_type.py
Normal file
64
authentik/stages/prompt/migrations/0012_alter_prompt_type.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-14 02:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_prompt",
|
||||
"0011_prompt_initial_value_prompt_initial_value_expression_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="prompt",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("text", "Text: Simple Text input"),
|
||||
("text_area", "Text area: Multiline Text Input."),
|
||||
(
|
||||
"text_read_only",
|
||||
"Text (read-only): Simple Text input, but cannot be edited.",
|
||||
),
|
||||
(
|
||||
"text_area_read_only",
|
||||
"Text area (read-only): Multiline Text input, but cannot be edited.",
|
||||
),
|
||||
(
|
||||
"username",
|
||||
"Username: Same as Text input, but checks for and prevents duplicate usernames.",
|
||||
),
|
||||
("email", "Email: Text field with Email type."),
|
||||
(
|
||||
"password",
|
||||
"Password: Masked input, multiple inputs of this type on the same prompt need to be identical.",
|
||||
),
|
||||
("number", "Number"),
|
||||
("checkbox", "Checkbox"),
|
||||
(
|
||||
"radio-button-group",
|
||||
"Fixed choice field rendered as a group of radio buttons.",
|
||||
),
|
||||
("dropdown", "Fixed choice field rendered as a dropdown."),
|
||||
("date", "Date"),
|
||||
("date-time", "Date Time"),
|
||||
(
|
||||
"file",
|
||||
"File: File upload for arbitrary files. File content will be available in flow context as data-URI",
|
||||
),
|
||||
("separator", "Separator: Static Separator Line"),
|
||||
("hidden", "Hidden: Hidden field, can be used to insert data into form."),
|
||||
("static", "Static: Static value, displayed as-is."),
|
||||
("alert_info", "Alert (Info): Static alert box with info styling"),
|
||||
("alert_warning", "Alert (Warning): Static alert box with warning styling"),
|
||||
("alert_danger", "Alert (Danger): Static alert box with danger styling"),
|
||||
("ak-locale", "authentik: Selection of locales authentik supports"),
|
||||
],
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -87,6 +87,11 @@ class FieldTypes(models.TextChoices):
|
||||
HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.")
|
||||
STATIC = "static", _("Static: Static value, displayed as-is.")
|
||||
|
||||
# Alert box types for displaying styled messages
|
||||
ALERT_INFO = "alert_info", _("Alert (Info): Static alert box with info styling")
|
||||
ALERT_WARNING = "alert_warning", _("Alert (Warning): Static alert box with warning styling")
|
||||
ALERT_DANGER = "alert_danger", _("Alert (Danger): Static alert box with danger styling")
|
||||
|
||||
AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
|
||||
|
||||
|
||||
@@ -299,7 +304,12 @@ class Prompt(SerializerModel):
|
||||
field_class = HiddenField
|
||||
kwargs["required"] = False
|
||||
kwargs["default"] = self.placeholder
|
||||
case FieldTypes.STATIC:
|
||||
case (
|
||||
FieldTypes.STATIC
|
||||
| FieldTypes.ALERT_INFO
|
||||
| FieldTypes.ALERT_WARNING
|
||||
| FieldTypes.ALERT_DANGER
|
||||
):
|
||||
kwargs["default"] = self.placeholder
|
||||
kwargs["required"] = False
|
||||
kwargs["label"] = ""
|
||||
|
||||
@@ -124,6 +124,9 @@ class PromptChallengeResponse(ChallengeResponse):
|
||||
type__in=[
|
||||
FieldTypes.HIDDEN,
|
||||
FieldTypes.STATIC,
|
||||
FieldTypes.ALERT_INFO,
|
||||
FieldTypes.ALERT_WARNING,
|
||||
FieldTypes.ALERT_DANGER,
|
||||
FieldTypes.TEXT_READ_ONLY,
|
||||
FieldTypes.TEXT_AREA_READ_ONLY,
|
||||
]
|
||||
|
||||
@@ -330,10 +330,20 @@ class TestPromptStage(FlowTestCase):
|
||||
|
||||
def test_static_hidden_overwrite(self):
|
||||
"""Test that static and hidden fields ignore any value sent to them"""
|
||||
alert_prompt = Prompt.objects.create(
|
||||
name=generate_id(),
|
||||
field_key="alert_prompt",
|
||||
type=FieldTypes.ALERT_INFO,
|
||||
required=True,
|
||||
placeholder="alert fallback",
|
||||
initial_value="alert content",
|
||||
)
|
||||
self.stage.fields.add(alert_prompt)
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {"hidden_prompt": "hidden"}
|
||||
self.prompt_data["hidden_prompt"] = "foo"
|
||||
self.prompt_data["static_prompt"] = "foo"
|
||||
self.prompt_data["alert_prompt"] = "foo"
|
||||
challenge_response = PromptChallengeResponse(
|
||||
None, stage_instance=self.stage, plan=plan, data=self.prompt_data, stage=self.stage_view
|
||||
)
|
||||
@@ -341,6 +351,7 @@ class TestPromptStage(FlowTestCase):
|
||||
self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo")
|
||||
self.assertEqual(challenge_response.validated_data["hidden_prompt"], "hidden")
|
||||
self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")
|
||||
self.assertEqual(challenge_response.validated_data["alert_prompt"], "alert content")
|
||||
|
||||
def test_prompt_placeholder(self):
|
||||
"""Test placeholder and expression"""
|
||||
|
||||
306
blueprints/example/flow-default-account-lockdown.yaml
Normal file
306
blueprints/example/flow-default-account-lockdown.yaml
Normal file
@@ -0,0 +1,306 @@
|
||||
version: 1
|
||||
metadata:
|
||||
name: Example - Account lockdown flow
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "false"
|
||||
entries:
|
||||
flows:
|
||||
# Main lockdown flow - requires authentication
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
name: Account Lockdown
|
||||
title: Lock Account
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-account-lockdown
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
||||
# Self-service completion flow - no authentication required
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
name: Account Lockdown Complete
|
||||
title: Account Locked
|
||||
authentication: none
|
||||
identifiers:
|
||||
slug: default-account-lockdown-complete
|
||||
model: authentik_flows.flow
|
||||
id: completion-flow
|
||||
prompt_fields:
|
||||
# Warning field - danger alert box (content varies based on self-service vs admin)
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
order: 50
|
||||
initial_value: |
|
||||
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||
pending_user = None
|
||||
if target_uuid and not is_self_service:
|
||||
from authentik.core.models import User
|
||||
|
||||
pending_user = User.objects.filter(pk=target_uuid).first()
|
||||
if is_self_service:
|
||||
return (
|
||||
"<p><strong>You are about to lock down your own account.</strong></p>"
|
||||
"<p>This is an emergency action for cutting off access to your account right away.</p>"
|
||||
"<p><strong>This will immediately:</strong></p>"
|
||||
"<ul>"
|
||||
"<li><strong>Invalidate your password</strong> - Your password will be set to a random value "
|
||||
"and cannot be recovered</li>"
|
||||
"<li><strong>Deactivate your account</strong> - Your account will be disabled</li>"
|
||||
"<li><strong>Terminate all your sessions</strong> - You will be logged out everywhere</li>"
|
||||
"<li><strong>Revoke all your tokens</strong> - All your API, app password, recovery, "
|
||||
"verification, and OAuth2 tokens and grants will be revoked</li>"
|
||||
"</ul>"
|
||||
"<p><strong>This action cannot be easily undone.</strong></p>"
|
||||
)
|
||||
|
||||
from django.utils.html import escape
|
||||
|
||||
if pending_user:
|
||||
email = escape(pending_user.email or pending_user.name or "No email")
|
||||
user_html = f"<p><code>{escape(pending_user.username)}</code> ({email})</p>"
|
||||
else:
|
||||
user_html = "<p>the account selected when this one-time lockdown link was created</p>"
|
||||
|
||||
return (
|
||||
"<p><strong>You are about to lock down the following account:</strong></p>"
|
||||
f"{user_html}"
|
||||
"<p>This is an emergency action for cutting off access to the account right away. "
|
||||
"It does not lock the administrator who opened this page.</p>"
|
||||
"<p><strong>This will immediately:</strong></p>"
|
||||
"<ul>"
|
||||
"<li>Invalidate the user's password</li>"
|
||||
"<li>Deactivate the user</li>"
|
||||
"<li>Terminate all sessions - All active sessions will be ended</li>"
|
||||
"<li>Revoke all tokens - All API, app password, recovery, verification, and OAuth2 "
|
||||
"tokens and grants will be revoked</li>"
|
||||
"</ul>"
|
||||
"<p><strong>This action cannot be easily undone.</strong></p>"
|
||||
)
|
||||
initial_value_expression: true
|
||||
required: false
|
||||
type: alert_danger
|
||||
field_key: lockdown_warning
|
||||
label: Warning
|
||||
sub_text: ""
|
||||
identifiers:
|
||||
name: default-account-lockdown-field-warning
|
||||
id: prompt-field-warning
|
||||
model: authentik_stages_prompt.prompt
|
||||
# Info field - when to use lockdown (content varies based on self-service vs admin)
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
order: 100
|
||||
initial_value: |
|
||||
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||
if is_self_service:
|
||||
info = (
|
||||
"Use this if you no longer trust your current password or sessions. "
|
||||
"After lockdown, you will need help from your administrator or security team to regain access."
|
||||
)
|
||||
else:
|
||||
info = (
|
||||
"Use this for incident response on the listed account, for example after a compromise report "
|
||||
"or suspicious activity. The reason you enter below will be recorded in the audit log."
|
||||
)
|
||||
return (
|
||||
f"<p>{info}</p>"
|
||||
'<p><a href="https://docs.goauthentik.io/docs/security/'
|
||||
'account-lockdown?utm_source=authentik" '
|
||||
'target="_blank" rel="noopener noreferrer">Learn more about account lockdown</a></p>'
|
||||
)
|
||||
initial_value_expression: true
|
||||
required: false
|
||||
type: alert_info
|
||||
field_key: lockdown_info
|
||||
label: Information
|
||||
sub_text: ""
|
||||
identifiers:
|
||||
name: default-account-lockdown-field-info
|
||||
id: prompt-field-info
|
||||
model: authentik_stages_prompt.prompt
|
||||
# Reason field - text area for lockdown reason
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
order: 200
|
||||
placeholder: |
|
||||
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||
if is_self_service:
|
||||
return "Describe why you are locking your account..."
|
||||
return "Describe why this account is being locked down..."
|
||||
placeholder_expression: true
|
||||
required: true
|
||||
type: text_area
|
||||
field_key: lockdown_reason
|
||||
label: Reason
|
||||
sub_text: This explanation will be recorded in the audit log.
|
||||
identifiers:
|
||||
name: default-account-lockdown-field-reason
|
||||
id: prompt-field-reason
|
||||
model: authentik_stages_prompt.prompt
|
||||
prompt_stages:
|
||||
# Prompt stage for warnings and reason input
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-warning
|
||||
- !KeyOf prompt-field-info
|
||||
- !KeyOf prompt-field-reason
|
||||
identifiers:
|
||||
name: default-account-lockdown-prompt
|
||||
id: default-account-lockdown-prompt
|
||||
model: authentik_stages_prompt.promptstage
|
||||
lockdown_stage:
|
||||
# Account lockdown stage - performs the actual lockdown
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
identifiers:
|
||||
name: default-account-lockdown-stage
|
||||
id: default-account-lockdown-stage
|
||||
model: authentik_stages_account_lockdown.accountlockdownstage
|
||||
attrs:
|
||||
deactivate_user: true
|
||||
set_unusable_password: true
|
||||
delete_sessions: true
|
||||
revoke_tokens: true
|
||||
self_service_completion_flow: !Find [authentik_flows.flow, [slug, default-account-lockdown-complete]]
|
||||
completion_prompt:
|
||||
# Completion message field - confirmation shown after an admin-triggered lockdown
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
order: 300
|
||||
initial_value: |
|
||||
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||
from django.utils.html import escape
|
||||
from authentik.core.models import User
|
||||
|
||||
if target_uuid:
|
||||
target = User.objects.filter(pk=target_uuid).first()
|
||||
if target:
|
||||
return f"<p><code>{escape(target.username)}</code> has been locked down.</p>"
|
||||
|
||||
return "<p>The selected account has been locked down.</p>"
|
||||
initial_value_expression: true
|
||||
required: false
|
||||
type: alert_info
|
||||
field_key: lockdown_complete
|
||||
label: Result
|
||||
sub_text: ""
|
||||
identifiers:
|
||||
name: default-account-lockdown-field-complete
|
||||
id: prompt-field-complete
|
||||
model: authentik_stages_prompt.prompt
|
||||
# Prompt stage for admin completion message
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-complete
|
||||
identifiers:
|
||||
name: default-account-lockdown-complete-prompt
|
||||
id: default-account-lockdown-complete-prompt
|
||||
model: authentik_stages_prompt.promptstage
|
||||
policies:
|
||||
# Expression policy to check if this is NOT a self-service lockdown (admin)
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
name: default-account-lockdown-admin-policy
|
||||
expression: |
|
||||
target_uuid = (request.http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||
current_user_uuid = str(getattr(request.user, "pk", "") or getattr(request.http_request.user, "pk", ""))
|
||||
return bool(target_uuid) and target_uuid != current_user_uuid
|
||||
identifiers:
|
||||
name: default-account-lockdown-admin-policy
|
||||
id: admin-policy
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
bindings:
|
||||
# Stage bindings
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
identifiers:
|
||||
order: 0
|
||||
stage: !KeyOf default-account-lockdown-prompt
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
identifiers:
|
||||
order: 10
|
||||
stage: !KeyOf default-account-lockdown-stage
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
# Admin completion stage binding - shown for admin lockdown only
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
identifiers:
|
||||
order: 20
|
||||
stage: !KeyOf default-account-lockdown-complete-prompt
|
||||
target: !KeyOf flow
|
||||
id: admin-completion-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
# Bind the admin policy to the admin completion stage
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
enabled: true
|
||||
negate: false
|
||||
order: 0
|
||||
identifiers:
|
||||
policy: !KeyOf admin-policy
|
||||
target: !KeyOf admin-completion-binding
|
||||
model: authentik_policies.policybinding
|
||||
self_service_completion:
|
||||
# Self-service completion message field (for the unauthenticated completion flow)
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
order: 100
|
||||
initial_value: |
|
||||
return (
|
||||
"<h1>Your account has been locked</h1>"
|
||||
"<p>You have been logged out of all sessions and your password has been invalidated.</p>"
|
||||
"<p>To regain access to your account, please contact your IT administrator or security team.</p>"
|
||||
)
|
||||
initial_value_expression: true
|
||||
required: false
|
||||
type: alert_warning
|
||||
field_key: self_lockdown_complete
|
||||
label: Account locked
|
||||
sub_text: ""
|
||||
identifiers:
|
||||
name: default-account-lockdown-self-field-complete
|
||||
id: self-prompt-field-complete
|
||||
model: authentik_stages_prompt.prompt
|
||||
# Prompt stage for self-service completion (unauthenticated)
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf self-prompt-field-complete
|
||||
identifiers:
|
||||
name: default-account-lockdown-self-complete-prompt
|
||||
id: default-account-lockdown-self-complete-prompt
|
||||
model: authentik_stages_prompt.promptstage
|
||||
# Bind self-service completion stage to the completion flow
|
||||
- conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
identifiers:
|
||||
order: 0
|
||||
stage: !KeyOf default-account-lockdown-self-complete-prompt
|
||||
target: !Find [authentik_flows.flow, [slug, default-account-lockdown-complete]]
|
||||
model: authentik_flows.flowstagebinding
|
||||
@@ -1216,6 +1216,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_account_lockdown.accountlockdownstage"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -5100,6 +5140,11 @@
|
||||
"format": "uuid",
|
||||
"title": "Flow device code"
|
||||
},
|
||||
"flow_lockdown": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Flow lockdown"
|
||||
},
|
||||
"default_application": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
@@ -6094,6 +6139,10 @@
|
||||
"authentik_sources_telegram.view_telegramsource",
|
||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||
"authentik_stages_account_lockdown.add_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.change_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.delete_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.view_accountlockdownstage",
|
||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.add_duodevice",
|
||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||
@@ -7757,6 +7806,69 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_account_lockdown.accountlockdownstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"deactivate_user": {
|
||||
"type": "boolean",
|
||||
"title": "Deactivate user",
|
||||
"description": "Deactivate the user account (set is_active to False)"
|
||||
},
|
||||
"set_unusable_password": {
|
||||
"type": "boolean",
|
||||
"title": "Set unusable password",
|
||||
"description": "Set an unusable password for the user"
|
||||
},
|
||||
"delete_sessions": {
|
||||
"type": "boolean",
|
||||
"title": "Delete sessions",
|
||||
"description": "Delete all active sessions for the user"
|
||||
},
|
||||
"revoke_tokens": {
|
||||
"type": "boolean",
|
||||
"title": "Revoke tokens",
|
||||
"description": "Revoke all tokens for the user (API, app password, recovery, verification, OAuth)"
|
||||
},
|
||||
"self_service_completion_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Self service completion flow",
|
||||
"description": "Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_account_lockdown.accountlockdownstage_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_accountlockdownstage",
|
||||
"change_accountlockdownstage",
|
||||
"delete_accountlockdownstage",
|
||||
"view_accountlockdownstage"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8952,6 +9064,7 @@
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.providers.ws_federation",
|
||||
"authentik.enterprise.reports",
|
||||
"authentik.enterprise.stages.account_lockdown",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.mtls",
|
||||
"authentik.enterprise.stages.source"
|
||||
@@ -9084,6 +9197,7 @@
|
||||
"authentik_providers_ssf.ssfprovider",
|
||||
"authentik_providers_ws_federation.wsfederationprovider",
|
||||
"authentik_reports.dataexport",
|
||||
"authentik_stages_account_lockdown.accountlockdownstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||
"authentik_stages_mtls.mutualtlsstage",
|
||||
"authentik_stages_source.sourcestage"
|
||||
@@ -11791,6 +11905,10 @@
|
||||
"authentik_sources_telegram.view_telegramsource",
|
||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||
"authentik_stages_account_lockdown.add_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.change_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.delete_accountlockdownstage",
|
||||
"authentik_stages_account_lockdown.view_accountlockdownstage",
|
||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.add_duodevice",
|
||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||
@@ -15657,6 +15775,9 @@
|
||||
"separator",
|
||||
"hidden",
|
||||
"static",
|
||||
"alert_info",
|
||||
"alert_warning",
|
||||
"alert_danger",
|
||||
"ak-locale"
|
||||
],
|
||||
"title": "Type"
|
||||
|
||||
4
lifecycle/aws/package-lock.json
generated
4
lifecycle/aws/package-lock.json
generated
@@ -13,8 +13,8 @@
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"node": ">=20",
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
|
||||
@@ -11,20 +11,7 @@
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
},
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"onFail": "warn",
|
||||
"version": ">=24"
|
||||
},
|
||||
"packageManager": {
|
||||
"name": "npm",
|
||||
"version": ">=11.10.1",
|
||||
"onFail": "warn"
|
||||
}
|
||||
},
|
||||
"packageManager": "npm@11.11.0+sha512.f36811c4aae1fde639527368ae44c571d050006a608d67a191f195a801a52637a312d259186254aa3a3799b05335b7390539cf28656d18f0591a1125ba35f973"
|
||||
"node": ">=20",
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,6 @@ ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
RUN --mount=type=bind,target=/work/package.json,src=./package.json \
|
||||
--mount=type=bind,target=/work/package-lock.json,src=./package-lock.json \
|
||||
--mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=bind,target=/work/scripts/node/,src=./scripts/node/ \
|
||||
--mount=type=bind,target=/work/packages/logger-js/,src=./packages/logger-js/ \
|
||||
node ./scripts/node/setup-corepack.mjs --force && \
|
||||
node ./scripts/node/lint-runtime.mjs ./web
|
||||
|
||||
WORKDIR /work/web
|
||||
|
||||
# These files need to be copied and cannot be mounted as `npm ci` will build the client's typescript
|
||||
@@ -29,7 +18,7 @@ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||
--mount=type=cache,id=npm-ak,sharing=shared,target=/root/.npm \
|
||||
corepack npm ci
|
||||
npm ci
|
||||
|
||||
COPY ./package.json /work
|
||||
COPY ./web /work/web/
|
||||
|
||||
@@ -10,22 +10,12 @@ WORKDIR /static
|
||||
COPY ./packages /packages
|
||||
COPY ./web/packages /static/packages
|
||||
|
||||
RUN --mount=type=bind,target=/static/package.json,src=./package.json \
|
||||
--mount=type=bind,target=/static/package-lock.json,src=./package-lock.json \
|
||||
--mount=type=bind,target=/static/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/static/web/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=bind,target=/static/scripts/node/,src=./scripts/node/ \
|
||||
--mount=type=bind,target=/static/packages/logger-js/,src=./packages/logger-js/ \
|
||||
node ./scripts/node/setup-corepack.mjs --force && \
|
||||
node ./scripts/node/lint-runtime.mjs ./web
|
||||
|
||||
COPY package.json /
|
||||
|
||||
RUN --mount=type=bind,target=/static/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/static/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=bind,target=/static/scripts,src=./web/scripts \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
corepack npm ci
|
||||
npm ci
|
||||
|
||||
COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
@@ -100,6 +100,7 @@ mod json {
|
||||
);
|
||||
|
||||
let mut json_layer = json_subscriber::fmt::layer()
|
||||
.with_level(false)
|
||||
.with_timer(LocalTime::new(time_format))
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
@@ -109,6 +110,11 @@ mod json {
|
||||
let inner_layer = json_layer.inner_layer_mut();
|
||||
inner_layer.with_thread_ids("thread_id");
|
||||
inner_layer.with_thread_names("thread_name");
|
||||
inner_layer.add_dynamic_field("level", |event, _| {
|
||||
Some(serde_json::Value::String(
|
||||
event.metadata().level().as_str().to_lowercase(),
|
||||
))
|
||||
});
|
||||
inner_layer.add_dynamic_field("pid", |_, _| {
|
||||
Some(serde_json::Value::Number(serde_json::Number::from(
|
||||
std::process::id(),
|
||||
|
||||
9
packages/client-go/api_core.go
generated
9
packages/client-go/api_core.go
generated
@@ -39,6 +39,7 @@ type ApiCoreBrandsListRequest struct {
|
||||
flowAuthentication *string
|
||||
flowDeviceCode *string
|
||||
flowInvalidation *string
|
||||
flowLockdown *string
|
||||
flowRecovery *string
|
||||
flowUnenrollment *string
|
||||
flowUserSettings *string
|
||||
@@ -104,6 +105,11 @@ func (r ApiCoreBrandsListRequest) FlowInvalidation(flowInvalidation string) ApiC
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiCoreBrandsListRequest) FlowLockdown(flowLockdown string) ApiCoreBrandsListRequest {
|
||||
r.flowLockdown = &flowLockdown
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiCoreBrandsListRequest) FlowRecovery(flowRecovery string) ApiCoreBrandsListRequest {
|
||||
r.flowRecovery = &flowRecovery
|
||||
return r
|
||||
@@ -230,6 +236,9 @@ func (a *CoreAPIService) CoreBrandsListExecute(r ApiCoreBrandsListRequest) (*Pag
|
||||
if r.flowInvalidation != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_invalidation", r.flowInvalidation, "form", "")
|
||||
}
|
||||
if r.flowLockdown != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_lockdown", r.flowLockdown, "form", "")
|
||||
}
|
||||
if r.flowRecovery != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_recovery", r.flowRecovery, "form", "")
|
||||
}
|
||||
|
||||
48
packages/client-go/model_brand.go
generated
48
packages/client-go/model_brand.go
generated
@@ -36,6 +36,7 @@ type Brand struct {
|
||||
FlowUnenrollment NullableString `json:"flow_unenrollment,omitempty"`
|
||||
FlowUserSettings NullableString `json:"flow_user_settings,omitempty"`
|
||||
FlowDeviceCode NullableString `json:"flow_device_code,omitempty"`
|
||||
FlowLockdown NullableString `json:"flow_lockdown,omitempty"`
|
||||
// When set, external users will be redirected to this application after authenticating.
|
||||
DefaultApplication NullableString `json:"default_application,omitempty"`
|
||||
// Web Certificate used by the authentik Core webserver.
|
||||
@@ -565,6 +566,49 @@ func (o *Brand) UnsetFlowDeviceCode() {
|
||||
o.FlowDeviceCode.Unset()
|
||||
}
|
||||
|
||||
// GetFlowLockdown returns the FlowLockdown field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *Brand) GetFlowLockdown() string {
|
||||
if o == nil || IsNil(o.FlowLockdown.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.FlowLockdown.Get()
|
||||
}
|
||||
|
||||
// GetFlowLockdownOk returns a tuple with the FlowLockdown field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *Brand) GetFlowLockdownOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.FlowLockdown.Get(), o.FlowLockdown.IsSet()
|
||||
}
|
||||
|
||||
// HasFlowLockdown returns a boolean if a field has been set.
|
||||
func (o *Brand) HasFlowLockdown() bool {
|
||||
if o != nil && o.FlowLockdown.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetFlowLockdown gets a reference to the given NullableString and assigns it to the FlowLockdown field.
|
||||
func (o *Brand) SetFlowLockdown(v string) {
|
||||
o.FlowLockdown.Set(&v)
|
||||
}
|
||||
|
||||
// SetFlowLockdownNil sets the value for FlowLockdown to be an explicit nil
|
||||
func (o *Brand) SetFlowLockdownNil() {
|
||||
o.FlowLockdown.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetFlowLockdown ensures that no value is present for FlowLockdown, not even an explicit nil
|
||||
func (o *Brand) UnsetFlowLockdown() {
|
||||
o.FlowLockdown.Unset()
|
||||
}
|
||||
|
||||
// GetDefaultApplication returns the DefaultApplication field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *Brand) GetDefaultApplication() string {
|
||||
if o == nil || IsNil(o.DefaultApplication.Get()) {
|
||||
@@ -763,6 +807,9 @@ func (o Brand) ToMap() (map[string]interface{}, error) {
|
||||
if o.FlowDeviceCode.IsSet() {
|
||||
toSerialize["flow_device_code"] = o.FlowDeviceCode.Get()
|
||||
}
|
||||
if o.FlowLockdown.IsSet() {
|
||||
toSerialize["flow_lockdown"] = o.FlowLockdown.Get()
|
||||
}
|
||||
if o.DefaultApplication.IsSet() {
|
||||
toSerialize["default_application"] = o.DefaultApplication.Get()
|
||||
}
|
||||
@@ -833,6 +880,7 @@ func (o *Brand) UnmarshalJSON(data []byte) (err error) {
|
||||
delete(additionalProperties, "flow_unenrollment")
|
||||
delete(additionalProperties, "flow_user_settings")
|
||||
delete(additionalProperties, "flow_device_code")
|
||||
delete(additionalProperties, "flow_lockdown")
|
||||
delete(additionalProperties, "default_application")
|
||||
delete(additionalProperties, "web_certificate")
|
||||
delete(additionalProperties, "client_certificates")
|
||||
|
||||
6
packages/client-go/model_prompt_type_enum.go
generated
6
packages/client-go/model_prompt_type_enum.go
generated
@@ -38,6 +38,9 @@ const (
|
||||
PROMPTTYPEENUM_SEPARATOR PromptTypeEnum = "separator"
|
||||
PROMPTTYPEENUM_HIDDEN PromptTypeEnum = "hidden"
|
||||
PROMPTTYPEENUM_STATIC PromptTypeEnum = "static"
|
||||
PROMPTTYPEENUM_ALERT_INFO PromptTypeEnum = "alert_info"
|
||||
PROMPTTYPEENUM_ALERT_WARNING PromptTypeEnum = "alert_warning"
|
||||
PROMPTTYPEENUM_ALERT_DANGER PromptTypeEnum = "alert_danger"
|
||||
PROMPTTYPEENUM_AK_LOCALE PromptTypeEnum = "ak-locale"
|
||||
)
|
||||
|
||||
@@ -60,6 +63,9 @@ var AllowedPromptTypeEnumEnumValues = []PromptTypeEnum{
|
||||
"separator",
|
||||
"hidden",
|
||||
"static",
|
||||
"alert_info",
|
||||
"alert_warning",
|
||||
"alert_danger",
|
||||
"ak-locale",
|
||||
}
|
||||
|
||||
|
||||
5
packages/client-rust/src/apis/core_api.rs
generated
5
packages/client-rust/src/apis/core_api.rs
generated
@@ -71,6 +71,7 @@ pub async fn core_brands_list(
|
||||
flow_authentication: Option<&str>,
|
||||
flow_device_code: Option<&str>,
|
||||
flow_invalidation: Option<&str>,
|
||||
flow_lockdown: Option<&str>,
|
||||
flow_recovery: Option<&str>,
|
||||
flow_unenrollment: Option<&str>,
|
||||
flow_user_settings: Option<&str>,
|
||||
@@ -92,6 +93,7 @@ pub async fn core_brands_list(
|
||||
let p_query_flow_authentication = flow_authentication;
|
||||
let p_query_flow_device_code = flow_device_code;
|
||||
let p_query_flow_invalidation = flow_invalidation;
|
||||
let p_query_flow_lockdown = flow_lockdown;
|
||||
let p_query_flow_recovery = flow_recovery;
|
||||
let p_query_flow_unenrollment = flow_unenrollment;
|
||||
let p_query_flow_user_settings = flow_user_settings;
|
||||
@@ -154,6 +156,9 @@ pub async fn core_brands_list(
|
||||
if let Some(ref param_value) = p_query_flow_invalidation {
|
||||
req_builder = req_builder.query(&[("flow_invalidation", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_flow_lockdown {
|
||||
req_builder = req_builder.query(&[("flow_lockdown", ¶m_value.to_string())]);
|
||||
}
|
||||
if let Some(ref param_value) = p_query_flow_recovery {
|
||||
req_builder = req_builder.query(&[("flow_recovery", ¶m_value.to_string())]);
|
||||
}
|
||||
|
||||
8
packages/client-rust/src/models/brand.rs
generated
8
packages/client-rust/src/models/brand.rs
generated
@@ -78,6 +78,13 @@ pub struct Brand {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub flow_device_code: Option<Option<uuid::Uuid>>,
|
||||
#[serde(
|
||||
rename = "flow_lockdown",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub flow_lockdown: Option<Option<uuid::Uuid>>,
|
||||
/// When set, external users will be redirected to this application after authenticating.
|
||||
#[serde(
|
||||
rename = "default_application",
|
||||
@@ -122,6 +129,7 @@ impl Brand {
|
||||
flow_unenrollment: None,
|
||||
flow_user_settings: None,
|
||||
flow_device_code: None,
|
||||
flow_lockdown: None,
|
||||
default_application: None,
|
||||
web_certificate: None,
|
||||
client_certificates: None,
|
||||
|
||||
@@ -47,6 +47,12 @@ pub enum PromptTypeEnum {
|
||||
Hidden,
|
||||
#[serde(rename = "static")]
|
||||
Static,
|
||||
#[serde(rename = "alert_info")]
|
||||
AlertInfo,
|
||||
#[serde(rename = "alert_warning")]
|
||||
AlertWarning,
|
||||
#[serde(rename = "alert_danger")]
|
||||
AlertDanger,
|
||||
#[serde(rename = "ak-locale")]
|
||||
AkLocale,
|
||||
}
|
||||
@@ -71,6 +77,9 @@ impl std::fmt::Display for PromptTypeEnum {
|
||||
Self::Separator => write!(f, "separator"),
|
||||
Self::Hidden => write!(f, "hidden"),
|
||||
Self::Static => write!(f, "static"),
|
||||
Self::AlertInfo => write!(f, "alert_info"),
|
||||
Self::AlertWarning => write!(f, "alert_warning"),
|
||||
Self::AlertDanger => write!(f, "alert_danger"),
|
||||
Self::AkLocale => write!(f, "ak-locale"),
|
||||
}
|
||||
}
|
||||
|
||||
71
packages/client-ts/src/apis/CoreApi.ts
generated
71
packages/client-ts/src/apis/CoreApi.ts
generated
@@ -52,6 +52,7 @@ import type {
|
||||
TransactionApplicationResponse,
|
||||
UsedBy,
|
||||
User,
|
||||
UserAccountLockdownRequest,
|
||||
UserAccountRequest,
|
||||
UserConsent,
|
||||
UserPasswordHashSetRequest,
|
||||
@@ -102,6 +103,7 @@ import {
|
||||
TransactionApplicationRequestToJSON,
|
||||
TransactionApplicationResponseFromJSON,
|
||||
UsedByFromJSON,
|
||||
UserAccountLockdownRequestToJSON,
|
||||
UserAccountRequestToJSON,
|
||||
UserConsentFromJSON,
|
||||
UserFromJSON,
|
||||
@@ -245,6 +247,7 @@ export interface CoreBrandsListRequest {
|
||||
flowAuthentication?: string;
|
||||
flowDeviceCode?: string;
|
||||
flowInvalidation?: string;
|
||||
flowLockdown?: string;
|
||||
flowRecovery?: string;
|
||||
flowUnenrollment?: string;
|
||||
flowUserSettings?: string;
|
||||
@@ -403,6 +406,10 @@ export interface CoreUserConsentUsedByListRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CoreUsersAccountLockdownCreateRequest {
|
||||
userAccountLockdownRequest?: UserAccountLockdownRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersCreateRequest {
|
||||
userRequest: UserRequest;
|
||||
}
|
||||
@@ -2214,6 +2221,10 @@ export class CoreApi extends runtime.BaseAPI {
|
||||
queryParameters["flow_invalidation"] = requestParameters["flowInvalidation"];
|
||||
}
|
||||
|
||||
if (requestParameters["flowLockdown"] != null) {
|
||||
queryParameters["flow_lockdown"] = requestParameters["flowLockdown"];
|
||||
}
|
||||
|
||||
if (requestParameters["flowRecovery"] != null) {
|
||||
queryParameters["flow_recovery"] = requestParameters["flowRecovery"];
|
||||
}
|
||||
@@ -4189,6 +4200,66 @@ export class CoreApi extends runtime.BaseAPI {
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersAccountLockdownCreate without sending the request
|
||||
*/
|
||||
async coreUsersAccountLockdownCreateRequestOpts(
|
||||
requestParameters: CoreUsersAccountLockdownCreateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/users/account_lockdown/`;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: UserAccountLockdownRequestToJSON(requestParameters["userAccountLockdownRequest"]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the target account, then return a flow link.
|
||||
*/
|
||||
async coreUsersAccountLockdownCreateRaw(
|
||||
requestParameters: CoreUsersAccountLockdownCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<Link>> {
|
||||
const requestOptions =
|
||||
await this.coreUsersAccountLockdownCreateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => LinkFromJSON(jsonValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the target account, then return a flow link.
|
||||
*/
|
||||
async coreUsersAccountLockdownCreate(
|
||||
requestParameters: CoreUsersAccountLockdownCreateRequest = {},
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<Link> {
|
||||
const response = await this.coreUsersAccountLockdownCreateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersCreate without sending the request
|
||||
*/
|
||||
|
||||
45
packages/client-ts/src/apis/LifecycleApi.ts
generated
45
packages/client-ts/src/apis/LifecycleApi.ts
generated
@@ -40,9 +40,12 @@ export interface LifecycleIterationsCreateRequest {
|
||||
lifecycleIterationRequest: LifecycleIterationRequest;
|
||||
}
|
||||
|
||||
export interface LifecycleIterationsLatestRetrieveRequest {
|
||||
export interface LifecycleIterationsListLatestRequest {
|
||||
contentType: string;
|
||||
objectId: string;
|
||||
ordering?: string;
|
||||
search?: string;
|
||||
userIsReviewer?: boolean;
|
||||
}
|
||||
|
||||
export interface LifecycleIterationsListOpenRequest {
|
||||
@@ -157,27 +160,39 @@ export class LifecycleApi extends runtime.BaseAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for lifecycleIterationsLatestRetrieve without sending the request
|
||||
* Creates request options for lifecycleIterationsListLatest without sending the request
|
||||
*/
|
||||
async lifecycleIterationsLatestRetrieveRequestOpts(
|
||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
||||
async lifecycleIterationsListLatestRequestOpts(
|
||||
requestParameters: LifecycleIterationsListLatestRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["contentType"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"contentType",
|
||||
'Required parameter "contentType" was null or undefined when calling lifecycleIterationsLatestRetrieve().',
|
||||
'Required parameter "contentType" was null or undefined when calling lifecycleIterationsListLatest().',
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters["objectId"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"objectId",
|
||||
'Required parameter "objectId" was null or undefined when calling lifecycleIterationsLatestRetrieve().',
|
||||
'Required parameter "objectId" was null or undefined when calling lifecycleIterationsListLatest().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
if (requestParameters["ordering"] != null) {
|
||||
queryParameters["ordering"] = requestParameters["ordering"];
|
||||
}
|
||||
|
||||
if (requestParameters["search"] != null) {
|
||||
queryParameters["search"] = requestParameters["search"];
|
||||
}
|
||||
|
||||
if (requestParameters["userIsReviewer"] != null) {
|
||||
queryParameters["user_is_reviewer"] = requestParameters["userIsReviewer"];
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
@@ -210,27 +225,27 @@ export class LifecycleApi extends runtime.BaseAPI {
|
||||
/**
|
||||
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
||||
*/
|
||||
async lifecycleIterationsLatestRetrieveRaw(
|
||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
||||
async lifecycleIterationsListLatestRaw(
|
||||
requestParameters: LifecycleIterationsListLatestRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<LifecycleIteration>> {
|
||||
): Promise<runtime.ApiResponse<Array<LifecycleIteration>>> {
|
||||
const requestOptions =
|
||||
await this.lifecycleIterationsLatestRetrieveRequestOpts(requestParameters);
|
||||
await this.lifecycleIterationsListLatestRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
LifecycleIterationFromJSON(jsonValue),
|
||||
jsonValue.map(LifecycleIterationFromJSON),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
||||
*/
|
||||
async lifecycleIterationsLatestRetrieve(
|
||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
||||
async lifecycleIterationsListLatest(
|
||||
requestParameters: LifecycleIterationsListLatestRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<LifecycleIteration> {
|
||||
const response = await this.lifecycleIterationsLatestRetrieveRaw(
|
||||
): Promise<Array<LifecycleIteration>> {
|
||||
const response = await this.lifecycleIterationsListLatestRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
|
||||
576
packages/client-ts/src/apis/StagesApi.ts
generated
576
packages/client-ts/src/apis/StagesApi.ts
generated
@@ -13,6 +13,8 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
AccountLockdownStage,
|
||||
AccountLockdownStageRequest,
|
||||
AuthenticatorAttachmentEnum,
|
||||
AuthenticatorDuoStage,
|
||||
AuthenticatorDuoStageDeviceImportResponse,
|
||||
@@ -61,6 +63,7 @@ import type {
|
||||
MutualTLSStageRequest,
|
||||
NetworkBindingEnum,
|
||||
NotConfiguredActionEnum,
|
||||
PaginatedAccountLockdownStageList,
|
||||
PaginatedAuthenticatorDuoStageList,
|
||||
PaginatedAuthenticatorEmailStageList,
|
||||
PaginatedAuthenticatorEndpointGDTCStageList,
|
||||
@@ -92,6 +95,7 @@ import type {
|
||||
PaginatedWebAuthnDeviceTypeList,
|
||||
PasswordStage,
|
||||
PasswordStageRequest,
|
||||
PatchedAccountLockdownStageRequest,
|
||||
PatchedAuthenticatorDuoStageRequest,
|
||||
PatchedAuthenticatorEmailStageRequest,
|
||||
PatchedAuthenticatorEndpointGDTCStageRequest,
|
||||
@@ -150,6 +154,8 @@ import type {
|
||||
WebAuthnDeviceType,
|
||||
} from "../models/index";
|
||||
import {
|
||||
AccountLockdownStageFromJSON,
|
||||
AccountLockdownStageRequestToJSON,
|
||||
AuthenticatorDuoStageDeviceImportResponseFromJSON,
|
||||
AuthenticatorDuoStageFromJSON,
|
||||
AuthenticatorDuoStageManualDeviceImportRequestToJSON,
|
||||
@@ -190,6 +196,7 @@ import {
|
||||
InvitationStageRequestToJSON,
|
||||
MutualTLSStageFromJSON,
|
||||
MutualTLSStageRequestToJSON,
|
||||
PaginatedAccountLockdownStageListFromJSON,
|
||||
PaginatedAuthenticatorDuoStageListFromJSON,
|
||||
PaginatedAuthenticatorEmailStageListFromJSON,
|
||||
PaginatedAuthenticatorEndpointGDTCStageListFromJSON,
|
||||
@@ -221,6 +228,7 @@ import {
|
||||
PaginatedWebAuthnDeviceTypeListFromJSON,
|
||||
PasswordStageFromJSON,
|
||||
PasswordStageRequestToJSON,
|
||||
PatchedAccountLockdownStageRequestToJSON,
|
||||
PatchedAuthenticatorDuoStageRequestToJSON,
|
||||
PatchedAuthenticatorEmailStageRequestToJSON,
|
||||
PatchedAuthenticatorEndpointGDTCStageRequestToJSON,
|
||||
@@ -273,6 +281,46 @@ import {
|
||||
} from "../models/index";
|
||||
import * as runtime from "../runtime";
|
||||
|
||||
export interface StagesAccountLockdownCreateRequest {
|
||||
accountLockdownStageRequest: AccountLockdownStageRequest;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownDestroyRequest {
|
||||
stageUuid: string;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownListRequest {
|
||||
deactivateUser?: boolean;
|
||||
deleteSessions?: boolean;
|
||||
name?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
revokeTokens?: boolean;
|
||||
search?: string;
|
||||
selfServiceCompletionFlow?: string;
|
||||
setUnusablePassword?: boolean;
|
||||
stageUuid?: string;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownPartialUpdateRequest {
|
||||
stageUuid: string;
|
||||
patchedAccountLockdownStageRequest?: PatchedAccountLockdownStageRequest;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownRetrieveRequest {
|
||||
stageUuid: string;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownUpdateRequest {
|
||||
stageUuid: string;
|
||||
accountLockdownStageRequest: AccountLockdownStageRequest;
|
||||
}
|
||||
|
||||
export interface StagesAccountLockdownUsedByListRequest {
|
||||
stageUuid: string;
|
||||
}
|
||||
|
||||
export interface StagesAllDestroyRequest {
|
||||
stageUuid: string;
|
||||
}
|
||||
@@ -1366,6 +1414,534 @@ export interface StagesUserWriteUsedByListRequest {
|
||||
*
|
||||
*/
|
||||
export class StagesApi extends runtime.BaseAPI {
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownCreate without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownCreateRequestOpts(
|
||||
requestParameters: StagesAccountLockdownCreateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["accountLockdownStageRequest"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"accountLockdownStageRequest",
|
||||
'Required parameter "accountLockdownStageRequest" was null or undefined when calling stagesAccountLockdownCreate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/`;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: AccountLockdownStageRequestToJSON(
|
||||
requestParameters["accountLockdownStageRequest"],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownCreateRaw(
|
||||
requestParameters: StagesAccountLockdownCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||
const requestOptions = await this.stagesAccountLockdownCreateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
AccountLockdownStageFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownCreate(
|
||||
requestParameters: StagesAccountLockdownCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<AccountLockdownStage> {
|
||||
const response = await this.stagesAccountLockdownCreateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownDestroy without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownDestroyRequestOpts(
|
||||
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["stageUuid"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"stageUuid",
|
||||
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownDestroy().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"stage_uuid"}}`,
|
||||
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "DELETE",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownDestroyRaw(
|
||||
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<void>> {
|
||||
const requestOptions =
|
||||
await this.stagesAccountLockdownDestroyRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.VoidApiResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownDestroy(
|
||||
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<void> {
|
||||
await this.stagesAccountLockdownDestroyRaw(requestParameters, initOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownList without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownListRequestOpts(
|
||||
requestParameters: StagesAccountLockdownListRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
if (requestParameters["deactivateUser"] != null) {
|
||||
queryParameters["deactivate_user"] = requestParameters["deactivateUser"];
|
||||
}
|
||||
|
||||
if (requestParameters["deleteSessions"] != null) {
|
||||
queryParameters["delete_sessions"] = requestParameters["deleteSessions"];
|
||||
}
|
||||
|
||||
if (requestParameters["name"] != null) {
|
||||
queryParameters["name"] = requestParameters["name"];
|
||||
}
|
||||
|
||||
if (requestParameters["ordering"] != null) {
|
||||
queryParameters["ordering"] = requestParameters["ordering"];
|
||||
}
|
||||
|
||||
if (requestParameters["page"] != null) {
|
||||
queryParameters["page"] = requestParameters["page"];
|
||||
}
|
||||
|
||||
if (requestParameters["pageSize"] != null) {
|
||||
queryParameters["page_size"] = requestParameters["pageSize"];
|
||||
}
|
||||
|
||||
if (requestParameters["revokeTokens"] != null) {
|
||||
queryParameters["revoke_tokens"] = requestParameters["revokeTokens"];
|
||||
}
|
||||
|
||||
if (requestParameters["search"] != null) {
|
||||
queryParameters["search"] = requestParameters["search"];
|
||||
}
|
||||
|
||||
if (requestParameters["selfServiceCompletionFlow"] != null) {
|
||||
queryParameters["self_service_completion_flow"] =
|
||||
requestParameters["selfServiceCompletionFlow"];
|
||||
}
|
||||
|
||||
if (requestParameters["setUnusablePassword"] != null) {
|
||||
queryParameters["set_unusable_password"] = requestParameters["setUnusablePassword"];
|
||||
}
|
||||
|
||||
if (requestParameters["stageUuid"] != null) {
|
||||
queryParameters["stage_uuid"] = requestParameters["stageUuid"];
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/`;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "GET",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownListRaw(
|
||||
requestParameters: StagesAccountLockdownListRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<PaginatedAccountLockdownStageList>> {
|
||||
const requestOptions = await this.stagesAccountLockdownListRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
PaginatedAccountLockdownStageListFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownList(
|
||||
requestParameters: StagesAccountLockdownListRequest = {},
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<PaginatedAccountLockdownStageList> {
|
||||
const response = await this.stagesAccountLockdownListRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownPartialUpdate without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownPartialUpdateRequestOpts(
|
||||
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["stageUuid"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"stageUuid",
|
||||
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownPartialUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"stage_uuid"}}`,
|
||||
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "PATCH",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: PatchedAccountLockdownStageRequestToJSON(
|
||||
requestParameters["patchedAccountLockdownStageRequest"],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownPartialUpdateRaw(
|
||||
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||
const requestOptions =
|
||||
await this.stagesAccountLockdownPartialUpdateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
AccountLockdownStageFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownPartialUpdate(
|
||||
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<AccountLockdownStage> {
|
||||
const response = await this.stagesAccountLockdownPartialUpdateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownRetrieve without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownRetrieveRequestOpts(
|
||||
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["stageUuid"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"stageUuid",
|
||||
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownRetrieve().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"stage_uuid"}}`,
|
||||
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "GET",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownRetrieveRaw(
|
||||
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||
const requestOptions =
|
||||
await this.stagesAccountLockdownRetrieveRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
AccountLockdownStageFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownRetrieve(
|
||||
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<AccountLockdownStage> {
|
||||
const response = await this.stagesAccountLockdownRetrieveRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownUpdate without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownUpdateRequestOpts(
|
||||
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["stageUuid"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"stageUuid",
|
||||
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters["accountLockdownStageRequest"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"accountLockdownStageRequest",
|
||||
'Required parameter "accountLockdownStageRequest" was null or undefined when calling stagesAccountLockdownUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"stage_uuid"}}`,
|
||||
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "PUT",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: AccountLockdownStageRequestToJSON(
|
||||
requestParameters["accountLockdownStageRequest"],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownUpdateRaw(
|
||||
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||
const requestOptions = await this.stagesAccountLockdownUpdateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
AccountLockdownStageFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Viewset
|
||||
*/
|
||||
async stagesAccountLockdownUpdate(
|
||||
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<AccountLockdownStage> {
|
||||
const response = await this.stagesAccountLockdownUpdateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAccountLockdownUsedByList without sending the request
|
||||
*/
|
||||
async stagesAccountLockdownUsedByListRequestOpts(
|
||||
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["stageUuid"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"stageUuid",
|
||||
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownUsedByList().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/stages/account_lockdown/{stage_uuid}/used_by/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"stage_uuid"}}`,
|
||||
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "GET",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all objects that use this object
|
||||
*/
|
||||
async stagesAccountLockdownUsedByListRaw(
|
||||
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<Array<UsedBy>>> {
|
||||
const requestOptions =
|
||||
await this.stagesAccountLockdownUsedByListRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(UsedByFromJSON));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all objects that use this object
|
||||
*/
|
||||
async stagesAccountLockdownUsedByList(
|
||||
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<Array<UsedBy>> {
|
||||
const response = await this.stagesAccountLockdownUsedByListRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for stagesAllDestroy without sending the request
|
||||
*/
|
||||
|
||||
166
packages/client-ts/src/models/AccountLockdownStage.ts
generated
Normal file
166
packages/client-ts/src/models/AccountLockdownStage.ts
generated
Normal file
@@ -0,0 +1,166 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { FlowSet } from "./FlowSet";
|
||||
import { FlowSetFromJSON } from "./FlowSet";
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Serializer
|
||||
* @export
|
||||
* @interface AccountLockdownStage
|
||||
*/
|
||||
export interface AccountLockdownStage {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly pk: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Get object type so that we know how to edit the object
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly component: string;
|
||||
/**
|
||||
* Return object's verbose_name
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly verboseName: string;
|
||||
/**
|
||||
* Return object's plural verbose_name
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly verboseNamePlural: string;
|
||||
/**
|
||||
* Return internal model name
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly metaModelName: string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<FlowSet>}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
readonly flowSet: Array<FlowSet>;
|
||||
/**
|
||||
* Deactivate the user account (set is_active to False)
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
deactivateUser?: boolean;
|
||||
/**
|
||||
* Set an unusable password for the user
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
setUnusablePassword?: boolean;
|
||||
/**
|
||||
* Delete all active sessions for the user
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
deleteSessions?: boolean;
|
||||
/**
|
||||
* Revoke all tokens for the user (API, app password, recovery, verification, OAuth)
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
revokeTokens?: boolean;
|
||||
/**
|
||||
* Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStage
|
||||
*/
|
||||
selfServiceCompletionFlow?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the AccountLockdownStage interface.
|
||||
*/
|
||||
export function instanceOfAccountLockdownStage(value: object): value is AccountLockdownStage {
|
||||
if (!("pk" in value) || value["pk"] === undefined) return false;
|
||||
if (!("name" in value) || value["name"] === undefined) return false;
|
||||
if (!("component" in value) || value["component"] === undefined) return false;
|
||||
if (!("verboseName" in value) || value["verboseName"] === undefined) return false;
|
||||
if (!("verboseNamePlural" in value) || value["verboseNamePlural"] === undefined) return false;
|
||||
if (!("metaModelName" in value) || value["metaModelName"] === undefined) return false;
|
||||
if (!("flowSet" in value) || value["flowSet"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function AccountLockdownStageFromJSON(json: any): AccountLockdownStage {
|
||||
return AccountLockdownStageFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function AccountLockdownStageFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): AccountLockdownStage {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
pk: json["pk"],
|
||||
name: json["name"],
|
||||
component: json["component"],
|
||||
verboseName: json["verbose_name"],
|
||||
verboseNamePlural: json["verbose_name_plural"],
|
||||
metaModelName: json["meta_model_name"],
|
||||
flowSet: (json["flow_set"] as Array<any>).map(FlowSetFromJSON),
|
||||
deactivateUser: json["deactivate_user"] == null ? undefined : json["deactivate_user"],
|
||||
setUnusablePassword:
|
||||
json["set_unusable_password"] == null ? undefined : json["set_unusable_password"],
|
||||
deleteSessions: json["delete_sessions"] == null ? undefined : json["delete_sessions"],
|
||||
revokeTokens: json["revoke_tokens"] == null ? undefined : json["revoke_tokens"],
|
||||
selfServiceCompletionFlow:
|
||||
json["self_service_completion_flow"] == null
|
||||
? undefined
|
||||
: json["self_service_completion_flow"],
|
||||
};
|
||||
}
|
||||
|
||||
export function AccountLockdownStageToJSON(json: any): AccountLockdownStage {
|
||||
return AccountLockdownStageToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function AccountLockdownStageToJSONTyped(
|
||||
value?: Omit<
|
||||
AccountLockdownStage,
|
||||
"pk" | "component" | "verbose_name" | "verbose_name_plural" | "meta_model_name" | "flow_set"
|
||||
> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
name: value["name"],
|
||||
deactivate_user: value["deactivateUser"],
|
||||
set_unusable_password: value["setUnusablePassword"],
|
||||
delete_sessions: value["deleteSessions"],
|
||||
revoke_tokens: value["revokeTokens"],
|
||||
self_service_completion_flow: value["selfServiceCompletionFlow"],
|
||||
};
|
||||
}
|
||||
114
packages/client-ts/src/models/AccountLockdownStageRequest.ts
generated
Normal file
114
packages/client-ts/src/models/AccountLockdownStageRequest.ts
generated
Normal file
@@ -0,0 +1,114 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Serializer
|
||||
* @export
|
||||
* @interface AccountLockdownStageRequest
|
||||
*/
|
||||
export interface AccountLockdownStageRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Deactivate the user account (set is_active to False)
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
deactivateUser?: boolean;
|
||||
/**
|
||||
* Set an unusable password for the user
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
setUnusablePassword?: boolean;
|
||||
/**
|
||||
* Delete all active sessions for the user
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
deleteSessions?: boolean;
|
||||
/**
|
||||
* Revoke all tokens for the user (API, app password, recovery, verification, OAuth)
|
||||
* @type {boolean}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
revokeTokens?: boolean;
|
||||
/**
|
||||
* Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.
|
||||
* @type {string}
|
||||
* @memberof AccountLockdownStageRequest
|
||||
*/
|
||||
selfServiceCompletionFlow?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the AccountLockdownStageRequest interface.
|
||||
*/
|
||||
export function instanceOfAccountLockdownStageRequest(
|
||||
value: object,
|
||||
): value is AccountLockdownStageRequest {
|
||||
if (!("name" in value) || value["name"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function AccountLockdownStageRequestFromJSON(json: any): AccountLockdownStageRequest {
|
||||
return AccountLockdownStageRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function AccountLockdownStageRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): AccountLockdownStageRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
name: json["name"],
|
||||
deactivateUser: json["deactivate_user"] == null ? undefined : json["deactivate_user"],
|
||||
setUnusablePassword:
|
||||
json["set_unusable_password"] == null ? undefined : json["set_unusable_password"],
|
||||
deleteSessions: json["delete_sessions"] == null ? undefined : json["delete_sessions"],
|
||||
revokeTokens: json["revoke_tokens"] == null ? undefined : json["revoke_tokens"],
|
||||
selfServiceCompletionFlow:
|
||||
json["self_service_completion_flow"] == null
|
||||
? undefined
|
||||
: json["self_service_completion_flow"],
|
||||
};
|
||||
}
|
||||
|
||||
export function AccountLockdownStageRequestToJSON(json: any): AccountLockdownStageRequest {
|
||||
return AccountLockdownStageRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function AccountLockdownStageRequestToJSONTyped(
|
||||
value?: AccountLockdownStageRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
name: value["name"],
|
||||
deactivate_user: value["deactivateUser"],
|
||||
set_unusable_password: value["setUnusablePassword"],
|
||||
delete_sessions: value["deleteSessions"],
|
||||
revoke_tokens: value["revokeTokens"],
|
||||
self_service_completion_flow: value["selfServiceCompletionFlow"],
|
||||
};
|
||||
}
|
||||
1
packages/client-ts/src/models/AppEnum.ts
generated
1
packages/client-ts/src/models/AppEnum.ts
generated
@@ -94,6 +94,7 @@ export const AppEnum = {
|
||||
AuthentikEnterpriseProvidersSsf: "authentik.enterprise.providers.ssf",
|
||||
AuthentikEnterpriseProvidersWsFederation: "authentik.enterprise.providers.ws_federation",
|
||||
AuthentikEnterpriseReports: "authentik.enterprise.reports",
|
||||
AuthentikEnterpriseStagesAccountLockdown: "authentik.enterprise.stages.account_lockdown",
|
||||
AuthentikEnterpriseStagesAuthenticatorEndpointGdtc:
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
AuthentikEnterpriseStagesMtls: "authentik.enterprise.stages.mtls",
|
||||
|
||||
8
packages/client-ts/src/models/Brand.ts
generated
8
packages/client-ts/src/models/Brand.ts
generated
@@ -102,6 +102,12 @@ export interface Brand {
|
||||
* @memberof Brand
|
||||
*/
|
||||
flowDeviceCode?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Brand
|
||||
*/
|
||||
flowLockdown?: string | null;
|
||||
/**
|
||||
* When set, external users will be redirected to this application after authenticating.
|
||||
* @type {string}
|
||||
@@ -166,6 +172,7 @@ export function BrandFromJSONTyped(json: any, ignoreDiscriminator: boolean): Bra
|
||||
flowUserSettings:
|
||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||
defaultApplication:
|
||||
json["default_application"] == null ? undefined : json["default_application"],
|
||||
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
||||
@@ -201,6 +208,7 @@ export function BrandToJSONTyped(
|
||||
flow_unenrollment: value["flowUnenrollment"],
|
||||
flow_user_settings: value["flowUserSettings"],
|
||||
flow_device_code: value["flowDeviceCode"],
|
||||
flow_lockdown: value["flowLockdown"],
|
||||
default_application: value["defaultApplication"],
|
||||
web_certificate: value["webCertificate"],
|
||||
client_certificates: value["clientCertificates"],
|
||||
|
||||
8
packages/client-ts/src/models/BrandRequest.ts
generated
8
packages/client-ts/src/models/BrandRequest.ts
generated
@@ -96,6 +96,12 @@ export interface BrandRequest {
|
||||
* @memberof BrandRequest
|
||||
*/
|
||||
flowDeviceCode?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BrandRequest
|
||||
*/
|
||||
flowLockdown?: string | null;
|
||||
/**
|
||||
* When set, external users will be redirected to this application after authenticating.
|
||||
* @type {string}
|
||||
@@ -158,6 +164,7 @@ export function BrandRequestFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
||||
flowUserSettings:
|
||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||
defaultApplication:
|
||||
json["default_application"] == null ? undefined : json["default_application"],
|
||||
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
||||
@@ -193,6 +200,7 @@ export function BrandRequestToJSONTyped(
|
||||
flow_unenrollment: value["flowUnenrollment"],
|
||||
flow_user_settings: value["flowUserSettings"],
|
||||
flow_device_code: value["flowDeviceCode"],
|
||||
flow_lockdown: value["flowLockdown"],
|
||||
default_application: value["defaultApplication"],
|
||||
web_certificate: value["webCertificate"],
|
||||
client_certificates: value["clientCertificates"],
|
||||
|
||||
8
packages/client-ts/src/models/CurrentBrand.ts
generated
8
packages/client-ts/src/models/CurrentBrand.ts
generated
@@ -117,6 +117,12 @@ export interface CurrentBrand {
|
||||
* @memberof CurrentBrand
|
||||
*/
|
||||
flowDeviceCode?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CurrentBrand
|
||||
*/
|
||||
flowLockdown?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -177,6 +183,7 @@ export function CurrentBrandFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
||||
flowUserSettings:
|
||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||
defaultLocale: json["default_locale"],
|
||||
flags: CurrentBrandFlagsFromJSON(json["flags"]),
|
||||
};
|
||||
@@ -213,6 +220,7 @@ export function CurrentBrandToJSONTyped(
|
||||
flow_unenrollment: value["flowUnenrollment"],
|
||||
flow_user_settings: value["flowUserSettings"],
|
||||
flow_device_code: value["flowDeviceCode"],
|
||||
flow_lockdown: value["flowLockdown"],
|
||||
flags: CurrentBrandFlagsToJSON(value["flags"]),
|
||||
};
|
||||
}
|
||||
|
||||
42
packages/client-ts/src/models/LifecycleIteration.ts
generated
42
packages/client-ts/src/models/LifecycleIteration.ts
generated
@@ -16,12 +16,10 @@ import type { ContentTypeEnum } from "./ContentTypeEnum";
|
||||
import { ContentTypeEnumFromJSON, ContentTypeEnumToJSON } from "./ContentTypeEnum";
|
||||
import type { LifecycleIterationStateEnum } from "./LifecycleIterationStateEnum";
|
||||
import { LifecycleIterationStateEnumFromJSON } from "./LifecycleIterationStateEnum";
|
||||
import type { RelatedRule } from "./RelatedRule";
|
||||
import { RelatedRuleFromJSON } from "./RelatedRule";
|
||||
import type { Review } from "./Review";
|
||||
import { ReviewFromJSON } from "./Review";
|
||||
import type { ReviewerGroup } from "./ReviewerGroup";
|
||||
import { ReviewerGroupFromJSON } from "./ReviewerGroup";
|
||||
import type { ReviewerUser } from "./ReviewerUser";
|
||||
import { ReviewerUserFromJSON } from "./ReviewerUser";
|
||||
|
||||
/**
|
||||
* Mixin to validate that a valid enterprise license
|
||||
@@ -90,30 +88,18 @@ export interface LifecycleIteration {
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly reviews: Array<Review>;
|
||||
/**
|
||||
*
|
||||
* @type {RelatedRule}
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly rule: RelatedRule;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly userCanReview: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ReviewerGroup>}
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly reviewerGroups: Array<ReviewerGroup>;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly minReviewers: number;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ReviewerUser>}
|
||||
* @memberof LifecycleIteration
|
||||
*/
|
||||
readonly reviewers: Array<ReviewerUser>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,10 +116,8 @@ export function instanceOfLifecycleIteration(value: object): value is LifecycleI
|
||||
if (!("gracePeriodEnd" in value) || value["gracePeriodEnd"] === undefined) return false;
|
||||
if (!("nextReviewDate" in value) || value["nextReviewDate"] === undefined) return false;
|
||||
if (!("reviews" in value) || value["reviews"] === undefined) return false;
|
||||
if (!("rule" in value) || value["rule"] === undefined) return false;
|
||||
if (!("userCanReview" in value) || value["userCanReview"] === undefined) return false;
|
||||
if (!("reviewerGroups" in value) || value["reviewerGroups"] === undefined) return false;
|
||||
if (!("minReviewers" in value) || value["minReviewers"] === undefined) return false;
|
||||
if (!("reviewers" in value) || value["reviewers"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -159,10 +143,8 @@ export function LifecycleIterationFromJSONTyped(
|
||||
gracePeriodEnd: new Date(json["grace_period_end"]),
|
||||
nextReviewDate: new Date(json["next_review_date"]),
|
||||
reviews: (json["reviews"] as Array<any>).map(ReviewFromJSON),
|
||||
rule: RelatedRuleFromJSON(json["rule"]),
|
||||
userCanReview: json["user_can_review"],
|
||||
reviewerGroups: (json["reviewer_groups"] as Array<any>).map(ReviewerGroupFromJSON),
|
||||
minReviewers: json["min_reviewers"],
|
||||
reviewers: (json["reviewers"] as Array<any>).map(ReviewerUserFromJSON),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,10 +164,8 @@ export function LifecycleIterationToJSONTyped(
|
||||
| "grace_period_end"
|
||||
| "next_review_date"
|
||||
| "reviews"
|
||||
| "rule"
|
||||
| "user_can_review"
|
||||
| "reviewer_groups"
|
||||
| "min_reviewers"
|
||||
| "reviewers"
|
||||
> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
|
||||
2
packages/client-ts/src/models/ModelEnum.ts
generated
2
packages/client-ts/src/models/ModelEnum.ts
generated
@@ -175,6 +175,8 @@ export const ModelEnum = {
|
||||
AuthentikProvidersWsFederationWsfederationprovider:
|
||||
"authentik_providers_ws_federation.wsfederationprovider",
|
||||
AuthentikReportsDataexport: "authentik_reports.dataexport",
|
||||
AuthentikStagesAccountLockdownAccountlockdownstage:
|
||||
"authentik_stages_account_lockdown.accountlockdownstage",
|
||||
AuthentikStagesAuthenticatorEndpointGdtcAuthenticatorendpointgdtcstage:
|
||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||
AuthentikStagesMtlsMutualtlsstage: "authentik_stages_mtls.mutualtlsstage",
|
||||
|
||||
97
packages/client-ts/src/models/PaginatedAccountLockdownStageList.ts
generated
Normal file
97
packages/client-ts/src/models/PaginatedAccountLockdownStageList.ts
generated
Normal file
@@ -0,0 +1,97 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { AccountLockdownStage } from "./AccountLockdownStage";
|
||||
import { AccountLockdownStageFromJSON, AccountLockdownStageToJSON } from "./AccountLockdownStage";
|
||||
import type { Pagination } from "./Pagination";
|
||||
import { PaginationFromJSON, PaginationToJSON } from "./Pagination";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PaginatedAccountLockdownStageList
|
||||
*/
|
||||
export interface PaginatedAccountLockdownStageList {
|
||||
/**
|
||||
*
|
||||
* @type {Pagination}
|
||||
* @memberof PaginatedAccountLockdownStageList
|
||||
*/
|
||||
pagination: Pagination;
|
||||
/**
|
||||
*
|
||||
* @type {Array<AccountLockdownStage>}
|
||||
* @memberof PaginatedAccountLockdownStageList
|
||||
*/
|
||||
results: Array<AccountLockdownStage>;
|
||||
/**
|
||||
*
|
||||
* @type {{ [key: string]: any; }}
|
||||
* @memberof PaginatedAccountLockdownStageList
|
||||
*/
|
||||
autocomplete: { [key: string]: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the PaginatedAccountLockdownStageList interface.
|
||||
*/
|
||||
export function instanceOfPaginatedAccountLockdownStageList(
|
||||
value: object,
|
||||
): value is PaginatedAccountLockdownStageList {
|
||||
if (!("pagination" in value) || value["pagination"] === undefined) return false;
|
||||
if (!("results" in value) || value["results"] === undefined) return false;
|
||||
if (!("autocomplete" in value) || value["autocomplete"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function PaginatedAccountLockdownStageListFromJSON(
|
||||
json: any,
|
||||
): PaginatedAccountLockdownStageList {
|
||||
return PaginatedAccountLockdownStageListFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PaginatedAccountLockdownStageListFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): PaginatedAccountLockdownStageList {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
pagination: PaginationFromJSON(json["pagination"]),
|
||||
results: (json["results"] as Array<any>).map(AccountLockdownStageFromJSON),
|
||||
autocomplete: json["autocomplete"],
|
||||
};
|
||||
}
|
||||
|
||||
export function PaginatedAccountLockdownStageListToJSON(
|
||||
json: any,
|
||||
): PaginatedAccountLockdownStageList {
|
||||
return PaginatedAccountLockdownStageListToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PaginatedAccountLockdownStageListToJSONTyped(
|
||||
value?: PaginatedAccountLockdownStageList | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
pagination: PaginationToJSON(value["pagination"]),
|
||||
results: (value["results"] as Array<any>).map(AccountLockdownStageToJSON),
|
||||
autocomplete: value["autocomplete"],
|
||||
};
|
||||
}
|
||||
117
packages/client-ts/src/models/PatchedAccountLockdownStageRequest.ts
generated
Normal file
117
packages/client-ts/src/models/PatchedAccountLockdownStageRequest.ts
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* AccountLockdownStage Serializer
|
||||
* @export
|
||||
* @interface PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
export interface PatchedAccountLockdownStageRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Deactivate the user account (set is_active to False)
|
||||
* @type {boolean}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
deactivateUser?: boolean;
|
||||
/**
|
||||
* Set an unusable password for the user
|
||||
* @type {boolean}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
setUnusablePassword?: boolean;
|
||||
/**
|
||||
* Delete all active sessions for the user
|
||||
* @type {boolean}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
deleteSessions?: boolean;
|
||||
/**
|
||||
* Revoke all tokens for the user (API, app password, recovery, verification, OAuth)
|
||||
* @type {boolean}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
revokeTokens?: boolean;
|
||||
/**
|
||||
* Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.
|
||||
* @type {string}
|
||||
* @memberof PatchedAccountLockdownStageRequest
|
||||
*/
|
||||
selfServiceCompletionFlow?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the PatchedAccountLockdownStageRequest interface.
|
||||
*/
|
||||
export function instanceOfPatchedAccountLockdownStageRequest(
|
||||
value: object,
|
||||
): value is PatchedAccountLockdownStageRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function PatchedAccountLockdownStageRequestFromJSON(
|
||||
json: any,
|
||||
): PatchedAccountLockdownStageRequest {
|
||||
return PatchedAccountLockdownStageRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PatchedAccountLockdownStageRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): PatchedAccountLockdownStageRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
name: json["name"] == null ? undefined : json["name"],
|
||||
deactivateUser: json["deactivate_user"] == null ? undefined : json["deactivate_user"],
|
||||
setUnusablePassword:
|
||||
json["set_unusable_password"] == null ? undefined : json["set_unusable_password"],
|
||||
deleteSessions: json["delete_sessions"] == null ? undefined : json["delete_sessions"],
|
||||
revokeTokens: json["revoke_tokens"] == null ? undefined : json["revoke_tokens"],
|
||||
selfServiceCompletionFlow:
|
||||
json["self_service_completion_flow"] == null
|
||||
? undefined
|
||||
: json["self_service_completion_flow"],
|
||||
};
|
||||
}
|
||||
|
||||
export function PatchedAccountLockdownStageRequestToJSON(
|
||||
json: any,
|
||||
): PatchedAccountLockdownStageRequest {
|
||||
return PatchedAccountLockdownStageRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PatchedAccountLockdownStageRequestToJSONTyped(
|
||||
value?: PatchedAccountLockdownStageRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
name: value["name"],
|
||||
deactivate_user: value["deactivateUser"],
|
||||
set_unusable_password: value["setUnusablePassword"],
|
||||
delete_sessions: value["deleteSessions"],
|
||||
revoke_tokens: value["revokeTokens"],
|
||||
self_service_completion_flow: value["selfServiceCompletionFlow"],
|
||||
};
|
||||
}
|
||||
@@ -96,6 +96,12 @@ export interface PatchedBrandRequest {
|
||||
* @memberof PatchedBrandRequest
|
||||
*/
|
||||
flowDeviceCode?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PatchedBrandRequest
|
||||
*/
|
||||
flowLockdown?: string | null;
|
||||
/**
|
||||
* When set, external users will be redirected to this application after authenticating.
|
||||
* @type {string}
|
||||
@@ -160,6 +166,7 @@ export function PatchedBrandRequestFromJSONTyped(
|
||||
flowUserSettings:
|
||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||
defaultApplication:
|
||||
json["default_application"] == null ? undefined : json["default_application"],
|
||||
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
||||
@@ -195,6 +202,7 @@ export function PatchedBrandRequestToJSONTyped(
|
||||
flow_unenrollment: value["flowUnenrollment"],
|
||||
flow_user_settings: value["flowUserSettings"],
|
||||
flow_device_code: value["flowDeviceCode"],
|
||||
flow_lockdown: value["flowLockdown"],
|
||||
default_application: value["defaultApplication"],
|
||||
web_certificate: value["webCertificate"],
|
||||
client_certificates: value["clientCertificates"],
|
||||
|
||||
3
packages/client-ts/src/models/PromptTypeEnum.ts
generated
3
packages/client-ts/src/models/PromptTypeEnum.ts
generated
@@ -34,6 +34,9 @@ export const PromptTypeEnum = {
|
||||
Separator: "separator",
|
||||
Hidden: "hidden",
|
||||
Static: "static",
|
||||
AlertInfo: "alert_info",
|
||||
AlertWarning: "alert_warning",
|
||||
AlertDanger: "alert_danger",
|
||||
AkLocale: "ak-locale",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
|
||||
103
packages/client-ts/src/models/RelatedRule.ts
generated
Normal file
103
packages/client-ts/src/models/RelatedRule.ts
generated
Normal file
@@ -0,0 +1,103 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { ReviewerGroup } from "./ReviewerGroup";
|
||||
import { ReviewerGroupFromJSON } from "./ReviewerGroup";
|
||||
import type { ReviewerUser } from "./ReviewerUser";
|
||||
import { ReviewerUserFromJSON } from "./ReviewerUser";
|
||||
|
||||
/**
|
||||
* Mixin to validate that a valid enterprise license
|
||||
* exists before allowing to save the object
|
||||
* @export
|
||||
* @interface RelatedRule
|
||||
*/
|
||||
export interface RelatedRule {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RelatedRule
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RelatedRule
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ReviewerGroup>}
|
||||
* @memberof RelatedRule
|
||||
*/
|
||||
readonly reviewerGroups: Array<ReviewerGroup>;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof RelatedRule
|
||||
*/
|
||||
readonly minReviewers: number;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ReviewerUser>}
|
||||
* @memberof RelatedRule
|
||||
*/
|
||||
readonly reviewers: Array<ReviewerUser>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the RelatedRule interface.
|
||||
*/
|
||||
export function instanceOfRelatedRule(value: object): value is RelatedRule {
|
||||
if (!("name" in value) || value["name"] === undefined) return false;
|
||||
if (!("reviewerGroups" in value) || value["reviewerGroups"] === undefined) return false;
|
||||
if (!("minReviewers" in value) || value["minReviewers"] === undefined) return false;
|
||||
if (!("reviewers" in value) || value["reviewers"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function RelatedRuleFromJSON(json: any): RelatedRule {
|
||||
return RelatedRuleFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RelatedRuleFromJSONTyped(json: any, ignoreDiscriminator: boolean): RelatedRule {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
id: json["id"] == null ? undefined : json["id"],
|
||||
name: json["name"],
|
||||
reviewerGroups: (json["reviewer_groups"] as Array<any>).map(ReviewerGroupFromJSON),
|
||||
minReviewers: json["min_reviewers"],
|
||||
reviewers: (json["reviewers"] as Array<any>).map(ReviewerUserFromJSON),
|
||||
};
|
||||
}
|
||||
|
||||
export function RelatedRuleToJSON(json: any): RelatedRule {
|
||||
return RelatedRuleToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RelatedRuleToJSONTyped(
|
||||
value?: Omit<RelatedRule, "reviewer_groups" | "min_reviewers" | "reviewers"> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
id: value["id"],
|
||||
name: value["name"],
|
||||
};
|
||||
}
|
||||
69
packages/client-ts/src/models/UserAccountLockdownRequest.ts
generated
Normal file
69
packages/client-ts/src/models/UserAccountLockdownRequest.ts
generated
Normal file
@@ -0,0 +1,69 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Choose the target account before starting the lockdown flow.
|
||||
* @export
|
||||
* @interface UserAccountLockdownRequest
|
||||
*/
|
||||
export interface UserAccountLockdownRequest {
|
||||
/**
|
||||
* User to lock. If omitted, locks the current user (self-service).
|
||||
* @type {number}
|
||||
* @memberof UserAccountLockdownRequest
|
||||
*/
|
||||
user?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserAccountLockdownRequest interface.
|
||||
*/
|
||||
export function instanceOfUserAccountLockdownRequest(
|
||||
value: object,
|
||||
): value is UserAccountLockdownRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserAccountLockdownRequestFromJSON(json: any): UserAccountLockdownRequest {
|
||||
return UserAccountLockdownRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAccountLockdownRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAccountLockdownRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
user: json["user"] == null ? undefined : json["user"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAccountLockdownRequestToJSON(json: any): UserAccountLockdownRequest {
|
||||
return UserAccountLockdownRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAccountLockdownRequestToJSONTyped(
|
||||
value?: UserAccountLockdownRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
user: value["user"],
|
||||
};
|
||||
}
|
||||
6
packages/client-ts/src/models/index.ts
generated
6
packages/client-ts/src/models/index.ts
generated
@@ -1,6 +1,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export * from "./AccessDeniedChallenge";
|
||||
export * from "./AccountLockdownStage";
|
||||
export * from "./AccountLockdownStageRequest";
|
||||
export * from "./AgentAuthenticationResponse";
|
||||
export * from "./AgentConfig";
|
||||
export * from "./AgentConnector";
|
||||
@@ -352,6 +354,7 @@ export * from "./OutpostHealth";
|
||||
export * from "./OutpostRequest";
|
||||
export * from "./OutpostTypeEnum";
|
||||
export * from "./PKCEMethodEnum";
|
||||
export * from "./PaginatedAccountLockdownStageList";
|
||||
export * from "./PaginatedAgentConnectorList";
|
||||
export * from "./PaginatedAppleIndependentSecureEnclaveList";
|
||||
export * from "./PaginatedApplicationEntitlementList";
|
||||
@@ -518,6 +521,7 @@ export * from "./PasswordPolicy";
|
||||
export * from "./PasswordPolicyRequest";
|
||||
export * from "./PasswordStage";
|
||||
export * from "./PasswordStageRequest";
|
||||
export * from "./PatchedAccountLockdownStageRequest";
|
||||
export * from "./PatchedAgentConnectorRequest";
|
||||
export * from "./PatchedAppleIndependentSecureEnclaveRequest";
|
||||
export * from "./PatchedApplicationEntitlementRequest";
|
||||
@@ -705,6 +709,7 @@ export * from "./RedirectURI";
|
||||
export * from "./RedirectURIRequest";
|
||||
export * from "./RedirectUriTypeEnum";
|
||||
export * from "./RelatedGroup";
|
||||
export * from "./RelatedRule";
|
||||
export * from "./Reputation";
|
||||
export * from "./ReputationPolicy";
|
||||
export * from "./ReputationPolicyRequest";
|
||||
@@ -821,6 +826,7 @@ export * from "./UsageEnum";
|
||||
export * from "./UsedBy";
|
||||
export * from "./UsedByActionEnum";
|
||||
export * from "./User";
|
||||
export * from "./UserAccountLockdownRequest";
|
||||
export * from "./UserAccountRequest";
|
||||
export * from "./UserAccountSerializerForRoleRequest";
|
||||
export * from "./UserAttributeEnum";
|
||||
|
||||
467
schema.yml
467
schema.yml
@@ -3172,6 +3172,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: flow_lockdown
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: flow_recovery
|
||||
schema:
|
||||
@@ -4585,6 +4590,35 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/account_lockdown/:
|
||||
post:
|
||||
operationId: core_users_account_lockdown_create
|
||||
description: Choose the target account, then return a flow link.
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAccountLockdownRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Link'
|
||||
examples:
|
||||
LockdownFlowURL:
|
||||
value:
|
||||
link: https://example.invalid/if/flow/default-account-lockdown/
|
||||
summary: Lockdown flow URL
|
||||
description: ''
|
||||
'400':
|
||||
description: No lockdown flow configured or the flow is not applicable
|
||||
'403':
|
||||
description: Permission denied (when targeting another user)
|
||||
/core/users/export/:
|
||||
post:
|
||||
operationId: core_users_export_create
|
||||
@@ -9132,7 +9166,7 @@ paths:
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/lifecycle/iterations/latest/{content_type}/{object_id}/:
|
||||
get:
|
||||
operationId: lifecycle_iterations_latest_retrieve
|
||||
operationId: lifecycle_iterations_list_latest
|
||||
description: |-
|
||||
Mixin to validate that a valid enterprise license
|
||||
exists before allowing to save the object
|
||||
@@ -9149,6 +9183,12 @@ paths:
|
||||
type: string
|
||||
pattern: ^[^/]+$
|
||||
required: true
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: user_is_reviewer
|
||||
schema:
|
||||
type: boolean
|
||||
tags:
|
||||
- lifecycle
|
||||
security:
|
||||
@@ -9158,7 +9198,9 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LifecycleIteration'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LifecycleIteration'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
@@ -26893,6 +26935,222 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/account_lockdown/:
|
||||
get:
|
||||
operationId: stages_account_lockdown_list
|
||||
description: AccountLockdownStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: deactivate_user
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: delete_sessions
|
||||
schema:
|
||||
type: boolean
|
||||
- $ref: '#/components/parameters/QueryName'
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- $ref: '#/components/parameters/QueryPaginationPage'
|
||||
- $ref: '#/components/parameters/QueryPaginationPageSize'
|
||||
- in: query
|
||||
name: revoke_tokens
|
||||
schema:
|
||||
type: boolean
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: self_service_completion_flow
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: set_unusable_password
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedAccountLockdownStageList'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
post:
|
||||
operationId: stages_account_lockdown_create
|
||||
description: AccountLockdownStage Viewset
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStage'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/account_lockdown/{stage_uuid}/:
|
||||
get:
|
||||
operationId: stages_account_lockdown_retrieve
|
||||
description: AccountLockdownStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Account Lockdown Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStage'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
put:
|
||||
operationId: stages_account_lockdown_update
|
||||
description: AccountLockdownStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Account Lockdown Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStage'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
patch:
|
||||
operationId: stages_account_lockdown_partial_update
|
||||
description: AccountLockdownStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Account Lockdown Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedAccountLockdownStageRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AccountLockdownStage'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
delete:
|
||||
operationId: stages_account_lockdown_destroy
|
||||
description: AccountLockdownStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Account Lockdown Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/account_lockdown/{stage_uuid}/used_by/:
|
||||
get:
|
||||
operationId: stages_account_lockdown_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Account Lockdown Stage.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UsedBy'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/stages/all/:
|
||||
get:
|
||||
operationId: stages_all_list
|
||||
@@ -33661,6 +33919,93 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
AccountLockdownStage:
|
||||
type: object
|
||||
description: AccountLockdownStage Serializer
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
title: Stage uuid
|
||||
name:
|
||||
type: string
|
||||
component:
|
||||
type: string
|
||||
description: Get object type so that we know how to edit the object
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
type: string
|
||||
description: Return object's plural verbose_name
|
||||
readOnly: true
|
||||
meta_model_name:
|
||||
type: string
|
||||
description: Return internal model name
|
||||
readOnly: true
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSet'
|
||||
readOnly: true
|
||||
deactivate_user:
|
||||
type: boolean
|
||||
description: Deactivate the user account (set is_active to False)
|
||||
set_unusable_password:
|
||||
type: boolean
|
||||
description: Set an unusable password for the user
|
||||
delete_sessions:
|
||||
type: boolean
|
||||
description: Delete all active sessions for the user
|
||||
revoke_tokens:
|
||||
type: boolean
|
||||
description: Revoke all tokens for the user (API, app password, recovery,
|
||||
verification, OAuth)
|
||||
self_service_completion_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow to redirect users to after self-service lockdown. This
|
||||
flow should not require authentication since the user's session is deleted.
|
||||
required:
|
||||
- component
|
||||
- flow_set
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
AccountLockdownStageRequest:
|
||||
type: object
|
||||
description: AccountLockdownStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
deactivate_user:
|
||||
type: boolean
|
||||
description: Deactivate the user account (set is_active to False)
|
||||
set_unusable_password:
|
||||
type: boolean
|
||||
description: Set an unusable password for the user
|
||||
delete_sessions:
|
||||
type: boolean
|
||||
description: Delete all active sessions for the user
|
||||
revoke_tokens:
|
||||
type: boolean
|
||||
description: Revoke all tokens for the user (API, app password, recovery,
|
||||
verification, OAuth)
|
||||
self_service_completion_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow to redirect users to after self-service lockdown. This
|
||||
flow should not require authentication since the user's session is deleted.
|
||||
required:
|
||||
- name
|
||||
AgentAuthenticationResponse:
|
||||
type: object
|
||||
description: Base serializer class which doesn't implement create/update methods
|
||||
@@ -33998,6 +34343,7 @@ components:
|
||||
- authentik.enterprise.providers.ssf
|
||||
- authentik.enterprise.providers.ws_federation
|
||||
- authentik.enterprise.reports
|
||||
- authentik.enterprise.stages.account_lockdown
|
||||
- authentik.enterprise.stages.authenticator_endpoint_gdtc
|
||||
- authentik.enterprise.stages.mtls
|
||||
- authentik.enterprise.stages.source
|
||||
@@ -35746,6 +36092,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
flow_lockdown:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
default_application:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -35818,6 +36168,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
flow_lockdown:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
default_application:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -36806,6 +37160,8 @@ components:
|
||||
type: string
|
||||
flow_device_code:
|
||||
type: string
|
||||
flow_lockdown:
|
||||
type: string
|
||||
default_locale:
|
||||
type: string
|
||||
readOnly: true
|
||||
@@ -42426,35 +42782,24 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/Review'
|
||||
readOnly: true
|
||||
rule:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RelatedRule'
|
||||
readOnly: true
|
||||
user_can_review:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
reviewer_groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReviewerGroup'
|
||||
readOnly: true
|
||||
min_reviewers:
|
||||
type: integer
|
||||
readOnly: true
|
||||
reviewers:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReviewerUser'
|
||||
readOnly: true
|
||||
required:
|
||||
- content_type
|
||||
- grace_period_end
|
||||
- id
|
||||
- min_reviewers
|
||||
- next_review_date
|
||||
- object_admin_url
|
||||
- object_id
|
||||
- object_verbose
|
||||
- opened_on
|
||||
- reviewer_groups
|
||||
- reviewers
|
||||
- reviews
|
||||
- rule
|
||||
- state
|
||||
- user_can_review
|
||||
LifecycleIterationRequest:
|
||||
@@ -43145,6 +43490,7 @@ components:
|
||||
- authentik_providers_ssf.ssfprovider
|
||||
- authentik_providers_ws_federation.wsfederationprovider
|
||||
- authentik_reports.dataexport
|
||||
- authentik_stages_account_lockdown.accountlockdownstage
|
||||
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
|
||||
- authentik_stages_mtls.mutualtlsstage
|
||||
- authentik_stages_source.sourcestage
|
||||
@@ -44587,6 +44933,21 @@ components:
|
||||
- plain
|
||||
- S256
|
||||
type: string
|
||||
PaginatedAccountLockdownStageList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AccountLockdownStage'
|
||||
autocomplete:
|
||||
$ref: '#/components/schemas/Autocomplete'
|
||||
required:
|
||||
- autocomplete
|
||||
- pagination
|
||||
- results
|
||||
PaginatedAgentConnectorList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -47355,6 +47716,32 @@ components:
|
||||
required:
|
||||
- backends
|
||||
- name
|
||||
PatchedAccountLockdownStageRequest:
|
||||
type: object
|
||||
description: AccountLockdownStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
deactivate_user:
|
||||
type: boolean
|
||||
description: Deactivate the user account (set is_active to False)
|
||||
set_unusable_password:
|
||||
type: boolean
|
||||
description: Set an unusable password for the user
|
||||
delete_sessions:
|
||||
type: boolean
|
||||
description: Delete all active sessions for the user
|
||||
revoke_tokens:
|
||||
type: boolean
|
||||
description: Revoke all tokens for the user (API, app password, recovery,
|
||||
verification, OAuth)
|
||||
self_service_completion_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow to redirect users to after self-service lockdown. This
|
||||
flow should not require authentication since the user's session is deleted.
|
||||
PatchedAgentConnectorRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -47798,6 +48185,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
flow_lockdown:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
default_application:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -52084,6 +52475,9 @@ components:
|
||||
- separator
|
||||
- hidden
|
||||
- static
|
||||
- alert_info
|
||||
- alert_warning
|
||||
- alert_danger
|
||||
- ak-locale
|
||||
type: string
|
||||
PropertyMapping:
|
||||
@@ -53285,6 +53679,35 @@ components:
|
||||
- group_uuid
|
||||
- name
|
||||
- pk
|
||||
RelatedRule:
|
||||
type: object
|
||||
description: |-
|
||||
Mixin to validate that a valid enterprise license
|
||||
exists before allowing to save the object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
reviewer_groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReviewerGroup'
|
||||
readOnly: true
|
||||
min_reviewers:
|
||||
type: integer
|
||||
readOnly: true
|
||||
reviewers:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReviewerUser'
|
||||
readOnly: true
|
||||
required:
|
||||
- min_reviewers
|
||||
- name
|
||||
- reviewer_groups
|
||||
- reviewers
|
||||
Reputation:
|
||||
type: object
|
||||
description: Reputation Serializer
|
||||
@@ -57153,6 +57576,14 @@ components:
|
||||
- uid
|
||||
- username
|
||||
- uuid
|
||||
UserAccountLockdownRequest:
|
||||
type: object
|
||||
description: Choose the target account before starting the lockdown flow.
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: User to lock. If omitted, locks the current user (self-service).
|
||||
UserAccountRequest:
|
||||
type: object
|
||||
description: Account adding/removing operations
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file Lints the package-lock.json file to ensure it is in sync with package.json.
|
||||
*
|
||||
* Usage:
|
||||
* lint-lockfile [options] [directory]
|
||||
*
|
||||
* Options:
|
||||
* --warn Report issues as warnings instead of failing. The lockfile is
|
||||
* still regenerated on disk, but the process exits 0.
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 Lockfile is in sync (or --warn was passed)
|
||||
* 1 Unexpected error
|
||||
* 2 Lockfile drift detected
|
||||
*/
|
||||
|
||||
/// <reference lib="esnext" />
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { findPackageJSON } from "node:module";
|
||||
import { dirname } from "node:path";
|
||||
import { isDeepStrictEqual, parseArgs } from "node:util";
|
||||
|
||||
import { ConsoleLogger } from "../../packages/logger-js/lib/node.js";
|
||||
import { parseCWD, reportAndExit } from "./utils/commands.mjs";
|
||||
import { corepack } from "./utils/corepack.mjs";
|
||||
import { gitStatus } from "./utils/git.mjs";
|
||||
import { findNPMPackage, loadJSON, npm, pluckDependencyFields } from "./utils/node.mjs";
|
||||
|
||||
//#region Constants
|
||||
|
||||
const logger = ConsoleLogger.prefix("lint:lockfile");
|
||||
|
||||
const { values: options, positionals } = parseArgs({
|
||||
options: {
|
||||
"warn": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "Report issues as warnings instead of failing",
|
||||
},
|
||||
"skip-git": {
|
||||
type: "boolean",
|
||||
default: !!process.env.CI,
|
||||
description:
|
||||
"Skip checking for uncommitted changes (use with --warn to ignore drift without reporting)",
|
||||
},
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
const cwd = parseCWD(positionals);
|
||||
|
||||
const ignoredProperties = new Set([
|
||||
// ---
|
||||
"peer",
|
||||
"engines",
|
||||
"optional",
|
||||
]);
|
||||
|
||||
//#region Utilities
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} actual
|
||||
* @param {Record<string, unknown>} expected
|
||||
* @param {string[]} [prefix]
|
||||
* @returns {Set<string>[]}
|
||||
*/
|
||||
function extractDiffedProperties(actual, expected, prefix = []) {
|
||||
const a = actual ?? {};
|
||||
const b = expected ?? {};
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
/** @type {Set<string>[]} */
|
||||
const diffs = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const path = [...prefix, key];
|
||||
const valA = a[key];
|
||||
const valB = b[key];
|
||||
|
||||
if (
|
||||
valA !== null &&
|
||||
valB !== null &&
|
||||
typeof valA === "object" &&
|
||||
typeof valB === "object" &&
|
||||
!Array.isArray(valA) &&
|
||||
!Array.isArray(valB)
|
||||
) {
|
||||
// @ts-ignore
|
||||
diffs.push(...extractDiffedProperties(valA, valB, path));
|
||||
} else if (!isDeepStrictEqual(valA, valB)) {
|
||||
diffs.push(new Set(path));
|
||||
}
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Exit code when lockfile drift is detected (distinct from general errors)
|
||||
*/
|
||||
const EXIT_DRIFT = 2;
|
||||
|
||||
/**
|
||||
* @returns {Promise<string[]>} The list of issues detected.
|
||||
*/
|
||||
async function run() {
|
||||
/** @type {string[]} */
|
||||
const issues = [];
|
||||
|
||||
/**
|
||||
* Records an issue. In strict mode, throws immediately.
|
||||
* In warn mode, collects the message for later reporting.
|
||||
*
|
||||
* @param {boolean} ok
|
||||
* @param {string} message
|
||||
*/
|
||||
const check = (ok, message) => {
|
||||
if (ok) return;
|
||||
|
||||
if (options.warn) {
|
||||
issues.push(message);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.fail(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks deep equality of two values. In strict mode, throws if they are not equal.
|
||||
* In warn mode, records an issue instead.
|
||||
*
|
||||
* @param {unknown} actual
|
||||
* @param {unknown} expected
|
||||
* @param {string} message
|
||||
*/
|
||||
const checkDeep = (actual, expected, message) => {
|
||||
if (options.warn) {
|
||||
if (!isDeepStrictEqual(actual, expected)) {
|
||||
issues.push(message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(actual, expected, message);
|
||||
};
|
||||
|
||||
logger.info(`Linting lockfile integrity in: ${cwd}`);
|
||||
|
||||
// MARK: Locate files
|
||||
|
||||
const resolvedPath = import.meta.resolve(cwd);
|
||||
const packageJSONPath = findPackageJSON(resolvedPath);
|
||||
|
||||
assert.ok(
|
||||
packageJSONPath,
|
||||
"Could not find package.json in the current directory or any parent directories",
|
||||
);
|
||||
|
||||
const packageDir = dirname(packageJSONPath);
|
||||
const { packageLockPath } = await findNPMPackage(packageDir);
|
||||
const lockfileDir = dirname(packageLockPath);
|
||||
const isWorkspace = lockfileDir !== packageDir;
|
||||
|
||||
const corepackVersion = await corepack`--version`().catch(() => null);
|
||||
const useCorepack = !!corepackVersion;
|
||||
logger.info(`corepack: ${corepackVersion || "disabled"}`);
|
||||
|
||||
const expected = {
|
||||
lockfile: await loadJSON(packageLockPath),
|
||||
package: await loadJSON(packageJSONPath).then(pluckDependencyFields),
|
||||
};
|
||||
|
||||
logger.info(`package.json: ${packageJSONPath} (${expected.package.name})`);
|
||||
logger.info(`package-lock.json: ${packageLockPath}${isWorkspace ? " (workspace root)" : ""}`);
|
||||
|
||||
// MARK: Uncommitted changes
|
||||
|
||||
if (options["skip-git"]) {
|
||||
logger.warn("Skipping git status check");
|
||||
} else {
|
||||
const packageStatus = await gitStatus(packageJSONPath);
|
||||
const lockfileStatus = await gitStatus(packageLockPath);
|
||||
|
||||
if (!packageStatus.available || !lockfileStatus.available) {
|
||||
logger.warn("Git is not available; skipping uncommitted change detection.");
|
||||
} else {
|
||||
check(packageStatus.clean, `package.json has uncommitted changes: ${packageJSONPath}`);
|
||||
|
||||
check(
|
||||
lockfileStatus.clean,
|
||||
`package-lock.json has uncommitted changes: ${packageLockPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Regenerate
|
||||
|
||||
const npmVersion = await npm`--version`({ useCorepack });
|
||||
|
||||
logger.info(`Detected npm version: ${npmVersion}`);
|
||||
|
||||
await npm`install --package-lock-only`({
|
||||
cwd: lockfileDir,
|
||||
useCorepack,
|
||||
});
|
||||
|
||||
logger.info("npm install complete.");
|
||||
|
||||
const actual = {
|
||||
lockfile: await loadJSON(packageLockPath),
|
||||
package: await loadJSON(packageJSONPath).then(pluckDependencyFields),
|
||||
};
|
||||
|
||||
// MARK: Compare
|
||||
|
||||
assert.deepStrictEqual(
|
||||
actual.package,
|
||||
expected.package,
|
||||
`package.json was unexpectedly modified during lockfile check: ${packageJSONPath}`,
|
||||
);
|
||||
|
||||
try {
|
||||
checkDeep(
|
||||
actual.lockfile,
|
||||
expected.lockfile,
|
||||
`package-lock.json is out of sync with package.json`,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof assert.AssertionError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// NPM versions <=11.10 has issues with deterministic lockfile generation,
|
||||
// especially around optional peer dependencies.
|
||||
const diffedProperties = extractDiffedProperties(actual.lockfile, expected.lockfile).filter(
|
||||
(segments) => segments.isDisjointFrom(ignoredProperties),
|
||||
);
|
||||
|
||||
if (diffedProperties.length) {
|
||||
const formatted = diffedProperties
|
||||
.map((segments) => Array.from(segments).join("."))
|
||||
.join("\n");
|
||||
|
||||
throw new Error(`Lockfile drift detected:\n${formatted}`, { cause: error });
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
"Permissible dependency differences detected. Run `npm install` to update the lockfile.",
|
||||
);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
run()
|
||||
.then((issues) => {
|
||||
if (issues.length) {
|
||||
logger.warn(`⚠️ ${issues.length} issue(s) detected:`);
|
||||
|
||||
for (const issue of issues) {
|
||||
logger.warn(` - ${issue}`);
|
||||
}
|
||||
|
||||
if (options.warn) {
|
||||
logger.warn(
|
||||
"The lockfile on disk has been regenerated. Review and commit the changes.",
|
||||
);
|
||||
process.exit(EXIT_DRIFT);
|
||||
}
|
||||
} else {
|
||||
logger.info("✅ Lockfile is in sync.");
|
||||
}
|
||||
})
|
||||
.catch((error) => reportAndExit(error, logger));
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file Lints the installed Node.js and npm versions against the requirements specified in package.json.
|
||||
*
|
||||
* Usage:
|
||||
* lint-node [options] [directory]
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 Versions are in sync
|
||||
* 1 Version mismatch detected
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { ConsoleLogger } from "../../packages/logger-js/lib/node.js";
|
||||
import { CommandError, parseCWD, reportAndExit } from "./utils/commands.mjs";
|
||||
import { corepack } from "./utils/corepack.mjs";
|
||||
import { resolveRepoRoot } from "./utils/git.mjs";
|
||||
import { compareVersions, findNPMPackage, loadJSON, node, npm, parseRange } from "./utils/node.mjs";
|
||||
|
||||
const logger = ConsoleLogger.prefix("lint-runtime");
|
||||
|
||||
/**
|
||||
* @param {string} start
|
||||
*/
|
||||
async function readRequirements(start) {
|
||||
const { packageJSONPath } = await findNPMPackage(start);
|
||||
|
||||
logger.info(`Checking versions in ${packageJSONPath}`);
|
||||
|
||||
const packageJSONData = await loadJSON(packageJSONPath);
|
||||
|
||||
const nodeVersion = await node`--version`().then((output) => output.replace(/^v/, ""));
|
||||
|
||||
const requiredNpmVersion = packageJSONData.engines?.npm;
|
||||
const requiredNodeVersion = packageJSONData.engines?.node;
|
||||
|
||||
return { nodeVersion, requiredNpmVersion, requiredNodeVersion };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const parsedArgs = parseArgs({
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
const cwd = parseCWD(parsedArgs.positionals);
|
||||
const repoRoot = await resolveRepoRoot(cwd).catch(() => null);
|
||||
|
||||
logger.info(`cwd ${cwd}`);
|
||||
logger.info(`repository ${repoRoot || "not found"}`);
|
||||
|
||||
const corepackVersion = await corepack`--version`().catch(() => null);
|
||||
const useCorepack = !!corepackVersion;
|
||||
logger.info(`corepack ${corepackVersion || "disabled"}`);
|
||||
|
||||
const npmVersion = await npm`--version`({ cwd, useCorepack })
|
||||
.then((version) => {
|
||||
logger.info(`npm${corepackVersion ? " (via Corepack)" : ""} ${version}`);
|
||||
|
||||
return version;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof CommandError && corepackVersion) {
|
||||
logger.warn(`Failed to read npm version via Corepack ${error.message}`);
|
||||
|
||||
logger.info(`Attempting to read npm version directly without Corepack...`);
|
||||
// Corepack might be misconfigured or outdated.
|
||||
// Attempting a second read without Corepack can help us distinguish
|
||||
// between a general npm issue and a Corepack-specific one.
|
||||
return npm`--version`({ cwd }).then((version) => {
|
||||
logger.info(`npm (direct) ${version}`);
|
||||
|
||||
return version;
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
const { nodeVersion, requiredNpmVersion, requiredNodeVersion } = await readRequirements(cwd);
|
||||
|
||||
logger.info(`node ${nodeVersion}`);
|
||||
|
||||
if (requiredNpmVersion) {
|
||||
logger.info(`package.json npm ${requiredNpmVersion}`);
|
||||
|
||||
const { operator, version: required } = parseRange(requiredNpmVersion);
|
||||
const result = compareVersions(npmVersion, required);
|
||||
|
||||
assert.ok(
|
||||
operator === ">=" ? result >= 0 : result === 0,
|
||||
`npm version ${npmVersion} does not satisfy required version ${requiredNpmVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (requiredNodeVersion) {
|
||||
logger.info(`package.json node ${requiredNodeVersion}`);
|
||||
|
||||
const { operator, version: required } = parseRange(requiredNodeVersion);
|
||||
const result = compareVersions(nodeVersion, required);
|
||||
|
||||
assert.ok(
|
||||
operator === ">=" ? result >= 0 : result === 0,
|
||||
`Node.js version ${nodeVersion} does not satisfy required version ${requiredNodeVersion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
logger.info("✅ Node.js and npm versions are in sync.");
|
||||
})
|
||||
.catch((error) => reportAndExit(error, logger));
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @file Downloads the latest corepack tarball from the npm registry.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { ConsoleLogger } from "../../packages/logger-js/lib/node.js";
|
||||
import { $, parseCWD, reportAndExit } from "./utils/commands.mjs";
|
||||
import { corepack, pullLatestCorepack } from "./utils/corepack.mjs";
|
||||
import { resolveRepoRoot } from "./utils/git.mjs";
|
||||
import { findNPMPackage, loadJSON, npm } from "./utils/node.mjs";
|
||||
|
||||
const FALLBACK_NPM_VERSION = "11.11.0";
|
||||
const logger = ConsoleLogger.prefix("setup-corepack");
|
||||
|
||||
async function main() {
|
||||
const parsedArgs = parseArgs({
|
||||
options: {
|
||||
force: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "Force re-download of corepack even if a version is already installed",
|
||||
},
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
const cwdArg = parseCWD(parsedArgs.positionals);
|
||||
|
||||
const repoRoot = await resolveRepoRoot(cwdArg).catch(() => null);
|
||||
const cwd = repoRoot || cwdArg;
|
||||
|
||||
const npmVersion = await npm`--version`({ cwd });
|
||||
|
||||
logger.info(`npm ${npmVersion}`);
|
||||
|
||||
const corepackVersion = await corepack`--version`({ cwd }).catch(() => null);
|
||||
|
||||
logger.info(`corepack ${corepackVersion || "not found"}`);
|
||||
|
||||
if (corepackVersion && !parsedArgs.values.force) {
|
||||
logger.info("Corepack is already installed, skipping download (use --force to override)");
|
||||
return;
|
||||
}
|
||||
|
||||
await pullLatestCorepack(cwd);
|
||||
|
||||
await npm`install --force -g corepack@latest`({ cwd });
|
||||
logger.info("Corepack installed successfully");
|
||||
|
||||
const { packageJSONPath } = await findNPMPackage(cwd);
|
||||
|
||||
logger.info(`Checking versions in ${packageJSONPath}`);
|
||||
|
||||
const packageJSONData = await loadJSON(packageJSONPath);
|
||||
|
||||
const packageManager = packageJSONData.packageManager || `npm@${FALLBACK_NPM_VERSION}`;
|
||||
|
||||
await $`corepack install -g ${packageManager}`({ cwd });
|
||||
|
||||
logger.info(`Setting up Corepack to use ${packageManager}...`);
|
||||
|
||||
const writablePackageJSON = await fs.access(packageJSONPath, fs.constants.W_OK).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let subcommand;
|
||||
|
||||
if (!writablePackageJSON) {
|
||||
if (!packageJSONData.packageManager) {
|
||||
throw new Error(
|
||||
`package.json is not writable and does not specify a packageManager field. Was the package.json file mounted via Docker?`,
|
||||
);
|
||||
}
|
||||
|
||||
subcommand = "install -g";
|
||||
} else {
|
||||
logger.info("package.json is writable");
|
||||
subcommand = "use";
|
||||
}
|
||||
|
||||
await $`corepack ${subcommand} ${packageManager}`({ cwd });
|
||||
|
||||
logger.info("Corepack installed npm successfully");
|
||||
}
|
||||
|
||||
main().catch((error) => reportAndExit(error, logger));
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* Utility functions for running shell commands and handling their results.
|
||||
*
|
||||
* @import { ExecOptions } from "node:child_process"
|
||||
*/
|
||||
|
||||
import { exec } from "node:child_process";
|
||||
import { resolve, sep } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { ConsoleLogger } from "../../../packages/logger-js/lib/node.js";
|
||||
|
||||
const logger = ConsoleLogger.prefix("commands");
|
||||
|
||||
export class CommandError extends Error {
|
||||
name = "CommandError";
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {ErrorOptions & ExecOptions} options
|
||||
*/
|
||||
constructor(command, { cause, cwd, shell } = {}) {
|
||||
const cwdInfo = cwd ? ` in directory ${cwd}` : "";
|
||||
const shellInfo = shell ? ` using shell ${shell}` : "";
|
||||
|
||||
super(`Command failed: ${command}${cwdInfo}${shellInfo}`, { cause });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} positionals
|
||||
* @returns {string} The resolved current working directory for the script
|
||||
*/
|
||||
export function parseCWD(positionals) {
|
||||
// `INIT_CWD` is present only if the script is run via npm.
|
||||
const initCWD = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
const cwd = (positionals.length ? resolve(initCWD, positionals[0]) : initCWD) + sep;
|
||||
|
||||
return cwd;
|
||||
}
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* @param {Awaited<ReturnType<typeof execAsync>>} result
|
||||
*/
|
||||
export const trimResult = (result) => String(result.stdout).trim();
|
||||
|
||||
/**
|
||||
* @typedef {(strings: TemplateStringsArray, ...expressions: unknown[]) =>
|
||||
* (options?: ExecOptions) => Promise<string>
|
||||
* } CommandTag
|
||||
*/
|
||||
|
||||
function createTag(prefix = "") {
|
||||
/** @type {CommandTag} */
|
||||
return (strings, ...expressions) => {
|
||||
const command = (prefix ? prefix + " " : "") + String.raw(strings, ...expressions);
|
||||
|
||||
logger.debug(command);
|
||||
|
||||
return (options) =>
|
||||
execAsync(command, options)
|
||||
.then(trimResult)
|
||||
.catch((cause) => {
|
||||
throw new CommandError(command, { ...options, cause });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A tagged template function for running shell commands.
|
||||
* @type {CommandTag & { bind(prefix: string): CommandTag }}
|
||||
*/
|
||||
export const $ = createTag();
|
||||
|
||||
/**
|
||||
* @param {string} prefix
|
||||
* @returns {CommandTag}
|
||||
*/
|
||||
$.bind = (prefix) => createTag(prefix);
|
||||
|
||||
/**
|
||||
* Promisified version of {@linkcode exec} for easier async/await usage.
|
||||
*
|
||||
* @param {string} command The command to run, with space-separated arguments.
|
||||
* @param {ExecOptions} [options] Optional execution options.
|
||||
* @throws {CommandError} If the command fails to execute.
|
||||
*/
|
||||
export function $2(command, options) {
|
||||
return execAsync(command, options)
|
||||
.then(trimResult)
|
||||
.catch((cause) => {
|
||||
throw new CommandError(command, { ...options, cause });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the given error and its cause (if any) and exits the process with a failure code.
|
||||
* @param {unknown} error
|
||||
* @param {typeof ConsoleLogger} logger
|
||||
* @returns {never}
|
||||
*/
|
||||
export function reportAndExit(error, logger = ConsoleLogger) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const cause = error instanceof Error && error.cause instanceof Error ? error.cause : null;
|
||||
|
||||
logger.error(`❌ ${message}`);
|
||||
|
||||
if (cause) {
|
||||
logger.error(`Caused by: ${cause.message}`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
import { ConsoleLogger } from "../../../packages/logger-js/lib/node.js";
|
||||
import { $ } from "./commands.mjs";
|
||||
|
||||
const REGISTRY_URL = "https://registry.npmjs.org/corepack";
|
||||
const OUTPUT_DIR = join(".corepack", "releases");
|
||||
const OUTPUT_FILENAME = "latest.tgz";
|
||||
|
||||
export const corepack = $.bind("corepack");
|
||||
|
||||
/**
|
||||
* Reads the installed Corepack version.
|
||||
*
|
||||
* @param {string} [cwd] The directory to run the command in.
|
||||
* @returns {Promise<string | null>} The installed Corepack version
|
||||
*/
|
||||
export function readCorepackVersion(cwd = process.cwd()) {
|
||||
return $`corepack --version`({ cwd });
|
||||
}
|
||||
|
||||
const logger = ConsoleLogger.prefix("setup-corepack");
|
||||
|
||||
/**
|
||||
* @param {string} baseDirectory
|
||||
*/
|
||||
export async function pullLatestCorepack(baseDirectory = process.cwd()) {
|
||||
logger.info("Fetching corepack metadata from registry...");
|
||||
|
||||
const outputDir = join(baseDirectory, OUTPUT_DIR);
|
||||
const outputPath = join(outputDir, OUTPUT_FILENAME);
|
||||
|
||||
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(1000 * 60) });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch registry metadata: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const metadata = await res.json();
|
||||
|
||||
const latestVersion = metadata["dist-tags"].latest;
|
||||
const versionData = metadata.versions[latestVersion];
|
||||
const tarballUrl = versionData.dist.tarball;
|
||||
const expectedIntegrity = versionData.dist.integrity;
|
||||
|
||||
logger.info(`Latest corepack version: ${latestVersion}`);
|
||||
logger.info(`Tarball URL: ${tarballUrl}`);
|
||||
logger.info(`Expected integrity: ${expectedIntegrity}`);
|
||||
|
||||
logger.info({ url: tarballUrl }, "Downloading tarball...");
|
||||
|
||||
const tarballRes = await fetch(tarballUrl, {
|
||||
signal: AbortSignal.timeout(1000 * 60),
|
||||
});
|
||||
|
||||
if (!tarballRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to download tarball: ${tarballRes.status} ${tarballRes.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const tarballBuffer = Buffer.from(await tarballRes.arrayBuffer());
|
||||
|
||||
logger.info("Verifying integrity...");
|
||||
|
||||
const [algorithm, expectedHash] = expectedIntegrity.split("-");
|
||||
const actualHash = crypto.createHash(algorithm).update(tarballBuffer).digest("base64");
|
||||
|
||||
if (actualHash !== expectedHash) {
|
||||
throw new Error(
|
||||
`Integrity mismatch!\n Expected: ${expectedHash}\n Actual: ${actualHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("Integrity verified.");
|
||||
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.writeFile(outputPath, tarballBuffer);
|
||||
|
||||
logger.info(`Saved to ${relative(baseDirectory, outputPath)}`);
|
||||
logger.info(`corepack@${latestVersion} (${expectedIntegrity})`);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { $ } from "./commands.mjs";
|
||||
|
||||
/**
|
||||
* Checks whether the given file has uncommitted changes in git.
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @param {string} [cwd]
|
||||
* @returns {Promise<{ clean: boolean, available: boolean }>}
|
||||
*/
|
||||
export async function gitStatus(filePath, cwd = process.cwd()) {
|
||||
return $`git status --porcelain ${filePath}`({ cwd })
|
||||
.then((output) => ({ clean: !output, available: true }))
|
||||
.catch(() => ({ clean: false, available: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the root directory of the git repository containing the given directory.
|
||||
*
|
||||
* @param {string} cwd
|
||||
* @returns {Promise<string>} The path to the git repository root.
|
||||
* @throws {Error} If the command fails (e.g., not a git repository).
|
||||
*/
|
||||
export function resolveRepoRoot(cwd = process.cwd()) {
|
||||
return $`git rev-parse --show-toplevel`({ cwd });
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* Utility functions for working with npm packages and versions.
|
||||
*
|
||||
* @import { ExecOptions } from "node:child_process"
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { $ } from "./commands.mjs";
|
||||
|
||||
/**
|
||||
* Find the nearest directory containing both package.json and package-lock.json,
|
||||
* starting from the given directory and walking upward.
|
||||
*
|
||||
* @param {string} start The directory to start searching from.
|
||||
* @returns {Promise<{ packageJSONPath: string, packageLockPath: string }>}
|
||||
* @throws {Error} If no co-located package.json and package-lock.json are found.
|
||||
*/
|
||||
export async function findNPMPackage(start) {
|
||||
let currentDir = start;
|
||||
|
||||
while (currentDir !== dirname(currentDir)) {
|
||||
const packageJSONPath = join(currentDir, "package.json");
|
||||
const packageLockPath = join(currentDir, "package-lock.json");
|
||||
|
||||
try {
|
||||
await Promise.all([fs.access(packageJSONPath), fs.access(packageLockPath)]);
|
||||
return {
|
||||
packageJSONPath,
|
||||
packageLockPath,
|
||||
};
|
||||
} catch {
|
||||
// Continue searching up the directory tree
|
||||
}
|
||||
|
||||
currentDir = dirname(currentDir);
|
||||
}
|
||||
|
||||
throw new Error(`No co-located package.json and package-lock.json found above ${start}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} PackageJSON
|
||||
* @property {string} name
|
||||
* @property {string} version
|
||||
* @property {Record<string, string>} [dependencies]
|
||||
* @property {Record<string, string>} [devDependencies]
|
||||
* @property {Record<string, string>} [peerDependencies]
|
||||
* @property {Record<string, string>} [optionalDependencies]
|
||||
* @property {Record<string, string>} [peerDependenciesMeta]
|
||||
* @property {Record<string, string>} [engines]
|
||||
* @property {Record<string, string>} [devEngines]
|
||||
* @property {string} [packageManager]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} jsonPath
|
||||
* @returns {Promise<PackageJSON>}
|
||||
*/
|
||||
export function loadJSON(jsonPath) {
|
||||
return fs
|
||||
.readFile(jsonPath, "utf-8")
|
||||
.then(JSON.parse)
|
||||
.catch((cause) => {
|
||||
throw new Error(`Failed to load JSON file at ${jsonPath}`, { cause });
|
||||
});
|
||||
}
|
||||
|
||||
const PackageJSONComparisionFields = /** @type {const} */ ([
|
||||
"name",
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"optionalDependencies",
|
||||
"peerDependencies",
|
||||
"peerDependenciesMeta",
|
||||
]);
|
||||
|
||||
/**
|
||||
* @typedef {typeof PackageJSONComparisionFields[number]} PackageJSONComparisionField
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts only the dependency fields from a package.json object for comparison purposes.
|
||||
*
|
||||
* @param {PackageJSON} data
|
||||
* @returns {Pick<PackageJSON, PackageJSONComparisionField>}
|
||||
*/
|
||||
export function pluckDependencyFields(data) {
|
||||
/**
|
||||
* @type {Record<string, unknown>}
|
||||
*/
|
||||
const result = {};
|
||||
|
||||
for (const field of PackageJSONComparisionFields) {
|
||||
if (data[field]) {
|
||||
result[field] = data[field];
|
||||
}
|
||||
}
|
||||
|
||||
return /** @type {Pick<PackageJSON, PackageJSONComparisionField>} */ (result);
|
||||
}
|
||||
|
||||
//#region Versioning
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings (e.g., "14.17.0").
|
||||
*
|
||||
* @param {string} a The first version string.
|
||||
* @param {string} b The second version string.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function compareVersions(a, b) {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (pa[i] > pb[i]) return 1;
|
||||
if (pa[i] < pb[i]) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a Node.js command and returns its stdout output as a string.
|
||||
*
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {...unknown} expressions
|
||||
* @returns {(options?: ExecOptions) => Promise<string>}
|
||||
*/
|
||||
export const node = $.bind("node");
|
||||
|
||||
/**
|
||||
* @typedef {object} NPMCommandOptions
|
||||
* @property {boolean} [useCorepack] Whether to prefix the command with "corepack " to use Corepack's shims.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Runs an npm command and returns its stdout output as a string.
|
||||
*
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {...unknown} expressions
|
||||
* @returns {(options?: ExecOptions & NPMCommandOptions) => Promise<string>}
|
||||
*/
|
||||
export function npm(strings, ...expressions) {
|
||||
const subcommand = String.raw(strings, ...expressions);
|
||||
|
||||
return ({ useCorepack, ...options } = {}) => {
|
||||
const command = [useCorepack ? "corepack" : "", "npm", subcommand]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return $`${command}`(options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version range string, stripping any leading >= and normalizing to three parts.
|
||||
* @param {string} range
|
||||
* @returns {{ operator: ">=" | "=", version: string }}
|
||||
*/
|
||||
export function parseRange(range) {
|
||||
const hasGte = range.startsWith(">=");
|
||||
const raw = hasGte ? range.slice(2) : range;
|
||||
const parts = raw.split(".").map(Number);
|
||||
|
||||
while (parts.length < 3) parts.push(0);
|
||||
|
||||
return {
|
||||
operator: hasGte ? ">=" : "=",
|
||||
version: parts.join("."),
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
2078
web/package-lock.json
generated
2078
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"format": "wireit",
|
||||
"lint": "eslint --fix .",
|
||||
"lint:imports": "knip --config scripts/knip.config.ts",
|
||||
"lint:lockfile": "wireit",
|
||||
"lint:types": "wireit",
|
||||
"lint-check": "eslint --max-warnings 0 .",
|
||||
"lit-analyse": "wireit",
|
||||
@@ -153,7 +154,7 @@
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"knip": "^6.6.3",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.2",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
@@ -267,6 +268,11 @@
|
||||
"build-locales"
|
||||
]
|
||||
},
|
||||
"lint:lockfile": {
|
||||
"__comment": "The lockfile-lint package does not have an option to ensure resolved hashes are set everywhere",
|
||||
"shell": true,
|
||||
"command": "sh ./scripts/lint-lockfile.sh package-lock.json"
|
||||
},
|
||||
"lit-analyse": {
|
||||
"command": "lit-analyzer src"
|
||||
},
|
||||
@@ -275,7 +281,8 @@
|
||||
"dependencies": [
|
||||
"lint",
|
||||
"lint:types",
|
||||
"lint:components"
|
||||
"lint:components",
|
||||
"lint:lockfile"
|
||||
]
|
||||
},
|
||||
"storybook:build": {
|
||||
@@ -296,7 +303,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
},
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
@@ -306,11 +313,10 @@
|
||||
},
|
||||
"packageManager": {
|
||||
"name": "npm",
|
||||
"version": ">=11.10.1",
|
||||
"version": "11.10.1",
|
||||
"onFail": "warn"
|
||||
}
|
||||
},
|
||||
"packageManager": "npm@11.11.0+sha512.f36811c4aae1fde639527368ae44c571d050006a608d67a191f195a801a52637a312d259186254aa3a3799b05335b7390539cf28656d18f0591a1125ba35f973",
|
||||
"prettier": "@goauthentik/prettier-config",
|
||||
"overrides": {
|
||||
"@goauthentik/esbuild-plugin-live-reload": {
|
||||
@@ -346,7 +352,6 @@
|
||||
"rapidoc": {
|
||||
"@apitools/openapi-parser": "0.0.37"
|
||||
},
|
||||
"tree-sitter": false,
|
||||
"typescript-eslint": {
|
||||
"typescript": "$typescript"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
21
web/scripts/lint-lockfile.sh
Executable file
21
web/scripts/lint-lockfile.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1 ; then
|
||||
echo "This check requires the jq program be installed."
|
||||
echo "To install jq, visit"
|
||||
echo " https://jqlang.github.io/jq/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CMD=$(jq -r '.packages | to_entries[] | select((.key | contains("node_modules")) and (.value | has("resolved") | not)) | .key' < "$1")
|
||||
|
||||
if [ -n "$CMD" ]; then
|
||||
echo "ERROR package-lock.json entries missing 'resolved' field:"
|
||||
echo ""
|
||||
# Shellcheck erroneously believes that shell string substitution can be used here, but that
|
||||
# feature lacks a "start of line" discriminator.
|
||||
# shellcheck disable=SC2001
|
||||
echo "$CMD" | sed 's/^/ /g'
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#admin/common/ak-crypto-certificate-search";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Alert";
|
||||
import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider";
|
||||
import "#elements/ak-dual-select/ak-dual-select-provider";
|
||||
import "#elements/forms/FormGroup";
|
||||
@@ -25,42 +26,79 @@ import {
|
||||
Brand,
|
||||
CoreApi,
|
||||
CoreApplicationsListRequest,
|
||||
Flow,
|
||||
FlowDesignationEnum,
|
||||
FlowsApi,
|
||||
UsageEnum,
|
||||
} from "@goauthentik/api";
|
||||
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum.js";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-brand-form")
|
||||
export class BrandForm extends ModelForm<Brand, string> {
|
||||
public static override verboseName = msg("Brand");
|
||||
public static override verboseNamePlural = msg("Brands");
|
||||
|
||||
loadInstance(pk: string): Promise<Brand> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsRetrieve({
|
||||
brandUuid: pk,
|
||||
#coreAPI = new CoreApi(DEFAULT_CONFIG);
|
||||
#flowsAPI = new FlowsApi(DEFAULT_CONFIG);
|
||||
|
||||
@state()
|
||||
protected lockdownFlowAuthentication: AuthenticationEnum | null = null;
|
||||
|
||||
async loadInstance(pk: string): Promise<Brand> {
|
||||
return this.#coreAPI.coreBrandsRetrieve({ brandUuid: pk }).then(async (brand) => {
|
||||
if (!brand.flowLockdown) {
|
||||
this.lockdownFlowAuthentication = null;
|
||||
|
||||
return brand;
|
||||
}
|
||||
|
||||
return this.#flowsAPI
|
||||
.flowsInstancesList({ flowUuid: brand.flowLockdown })
|
||||
.then((flows) => {
|
||||
this.lockdownFlowAuthentication = flows.results[0]?.authentication ?? null;
|
||||
|
||||
return brand;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
protected lockdownFlowInputListener = (event: Event): void => {
|
||||
const target = event.currentTarget as HTMLElement & {
|
||||
selectedFlow?: Flow | null;
|
||||
};
|
||||
this.lockdownFlowAuthentication = target.selectedFlow?.authentication ?? null;
|
||||
};
|
||||
|
||||
protected get lockdownWarningVisible(): boolean {
|
||||
return !!(
|
||||
this.lockdownFlowAuthentication &&
|
||||
this.lockdownFlowAuthentication !== AuthenticationEnum.RequireAuthenticated
|
||||
);
|
||||
}
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated brand.")
|
||||
: msg("Successfully created brand.");
|
||||
}
|
||||
|
||||
async send(data: Brand): Promise<Brand> {
|
||||
protected override async send(data: Brand): Promise<Brand> {
|
||||
data.attributes ??= {};
|
||||
|
||||
if (this.instance?.brandUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsPartialUpdate({
|
||||
return this.#coreAPI.coreBrandsPartialUpdate({
|
||||
brandUuid: this.instance.brandUuid,
|
||||
patchedBrandRequest: data,
|
||||
});
|
||||
}
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
|
||||
|
||||
return this.#coreAPI.coreBrandsCreate({
|
||||
brandRequest: data,
|
||||
});
|
||||
}
|
||||
@@ -285,6 +323,29 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Account lockdown flow")}
|
||||
name="flowLockdown"
|
||||
>
|
||||
<ak-flow-search
|
||||
placeholder=${msg("Select an account lockdown flow...")}
|
||||
flowType=${FlowDesignationEnum.StageConfiguration}
|
||||
.currentFlow=${this.instance?.flowLockdown}
|
||||
@input=${this.lockdownFlowInputListener}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user triggers account lockdown (e.g. in case of compromise). Should contain an Account Lockdown stage.",
|
||||
)}
|
||||
</p>
|
||||
${this.lockdownWarningVisible
|
||||
? html`<ak-alert inline>
|
||||
${msg(
|
||||
"Account lockdown flows should require authentication so they can only be started from a signed-in session.",
|
||||
)}
|
||||
</ak-alert>`
|
||||
: null}
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label="${msg("Other global settings")} ">
|
||||
|
||||
@@ -104,7 +104,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"If no group is selected and 'Send notification to event user' is disabled the rule is disabled. ",
|
||||
"If no group is selected and 'Send notification to event user' is disabled, the rule is disabled.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
@@ -113,7 +113,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
|
||||
label=${msg("Send notification to event user")}
|
||||
?checked=${this.instance?.destinationEventUser ?? false}
|
||||
help=${msg(
|
||||
"When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport. If no group is selected and 'Send notification to event user' is disabled the rule is disabled. ",
|
||||
"When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
@@ -80,7 +80,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
|
||||
protected override row(item: NotificationRule): SlottedTemplateResult[] {
|
||||
const enabled = !!item.destinationGroupObj || item.destinationEventUser;
|
||||
return [
|
||||
html`<ak-status-label type="warning" ?good=${enabled}></ak-status-label>`,
|
||||
html`<ak-status-label ?good=${enabled}></ak-status-label>`,
|
||||
html`${item.name}`,
|
||||
html`${severityToLabel(item.severity)}`,
|
||||
html`${item.destinationGroupObj
|
||||
|
||||
@@ -234,7 +234,7 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string, Lifecycl
|
||||
${this.renderReviewerGroupsSelection()}
|
||||
</ak-form-element-horizontal>
|
||||
<ak-number-input
|
||||
label=${msg("Min reviewers")}
|
||||
label=${msg("Minimum reviewers")}
|
||||
min=${1}
|
||||
name="minReviewers"
|
||||
value="${this.instance?.minReviewers ?? 1}"
|
||||
@@ -245,7 +245,7 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string, Lifecycl
|
||||
<ak-switch-input
|
||||
name="minReviewersIsPerGroup"
|
||||
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
|
||||
label=${msg("Min reviewers is per-group")}
|
||||
label=${msg("Minimum reviewers is per-group")}
|
||||
.help=${msg(
|
||||
html`If checked, approving a review will require at least that many users from
|
||||
<em>each</em> of the selected groups. When disabled, the value is a total
|
||||
|
||||
@@ -26,7 +26,6 @@ import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-lifecycle-rule-list")
|
||||
export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
|
||||
public override expandable = true;
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
public override searchPlaceholder = msg("Search for a lifecycle rule by name or target...");
|
||||
@@ -95,26 +94,6 @@ export class LifecycleRuleListPage extends TablePage<LifecycleRule> {
|
||||
];
|
||||
}
|
||||
|
||||
protected override renderExpanded(item: LifecycleRule): SlottedTemplateResult {
|
||||
const [appLabel, modelName] = ModelEnum.AuthentikLifecycleLifecyclerule.split(".");
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Tasks")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-task-list
|
||||
search-placeholder=${msg("Search tasks...")}
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${item.id}"
|
||||
></ak-task-list>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>`;
|
||||
}
|
||||
protected override renderObjectCreate(): SlottedTemplateResult {
|
||||
return ModalInvokerButton(LifecycleRuleForm);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
import "#admin/lifecycle/LifecyclePreviewBanner";
|
||||
import "#components/ak-textarea-input";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/timestamp/ak-timestamp";
|
||||
import "#admin/lifecycle/ObjectReviewForm";
|
||||
import "#admin/lifecycle/ObjectReviewIteration";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { createPaginatedResponse } from "#common/api/responses";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { isResponseErrorLike } from "#common/errors/network";
|
||||
|
||||
import { ModalInvokerButton } from "#elements/dialogs";
|
||||
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
import Styles from "#elements/table/Table.css";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPreviousValue } from "#elements/utils/properties";
|
||||
|
||||
import { ObjectReviewForm } from "#admin/lifecycle/ObjectReviewForm";
|
||||
import { LifecycleIterationStatus } from "#admin/lifecycle/utils";
|
||||
import { ContentTypeEnum, LifecycleApi, LifecycleIteration } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
ContentTypeEnum,
|
||||
LifecycleApi,
|
||||
LifecycleIteration,
|
||||
LifecycleIterationStateEnum,
|
||||
Review,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||
|
||||
@customElement("ak-object-lifecycle-page")
|
||||
export class ObjectLifecyclePage extends Table<Review> {
|
||||
export class ObjectLifecyclePage extends WithLicenseSummary(WithSession(AKElement)) {
|
||||
static styles = [
|
||||
// ---
|
||||
...super.styles,
|
||||
PFTitle,
|
||||
PFGrid,
|
||||
PFBanner,
|
||||
PFCard,
|
||||
PFFlex,
|
||||
Styles,
|
||||
PFSpacing,
|
||||
PFPage,
|
||||
PFDescriptionList,
|
||||
];
|
||||
|
||||
//#region Public Properties
|
||||
@@ -58,237 +44,67 @@ export class ObjectLifecyclePage extends Table<Review> {
|
||||
@property({ attribute: "object-pk", hasChanged: ifPreviousValue, useDefault: true })
|
||||
public objectPk: string | number | null = null;
|
||||
|
||||
public override paginated = false;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Protected Properties
|
||||
|
||||
protected override emptyStateMessage = msg("No reviews yet.");
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
[msg("Reviewed on"), "timestamp"],
|
||||
[msg("Reviewer"), "reviewer"],
|
||||
[msg("Note"), "note"],
|
||||
];
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@state()
|
||||
protected iteration: LifecycleIteration | null = null;
|
||||
protected iterations: LifecycleIteration[] | null = null;
|
||||
|
||||
protected apiEndpoint(): Promise<PaginatedResponse<Review>> {
|
||||
#refreshListener = () => {
|
||||
return this.fetch();
|
||||
};
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(EVENT_REFRESH, this.#refreshListener);
|
||||
}
|
||||
|
||||
public async fetch(): Promise<void> {
|
||||
if (!this.model || !this.objectPk) {
|
||||
return Promise.resolve(createPaginatedResponse<Review>());
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new LifecycleApi(DEFAULT_CONFIG)
|
||||
.lifecycleIterationsLatestRetrieve({
|
||||
.lifecycleIterationsListLatest({
|
||||
contentType: this.model,
|
||||
objectId: String(this.objectPk),
|
||||
})
|
||||
.then((iteration) => {
|
||||
this.iteration = iteration;
|
||||
|
||||
return createPaginatedResponse(iteration.reviews);
|
||||
.then((iterations) => {
|
||||
this.iterations = iterations;
|
||||
})
|
||||
.catch(async (error: unknown) => {
|
||||
if (isResponseErrorLike(error) && error.response.status === 404) {
|
||||
this.iteration = null;
|
||||
|
||||
return createPaginatedResponse<Review>();
|
||||
this.iterations = null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("model") || changedProperties.has("objectPk")) {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
//#region Summary Card
|
||||
|
||||
protected renderReviewers(): SlottedTemplateResult {
|
||||
if (!this.iteration) {
|
||||
return html`<span>${msg("No review iteration found for this object.")}</span>`;
|
||||
}
|
||||
|
||||
const { reviewers, reviewerGroups, minReviewers } = this.iteration;
|
||||
|
||||
const result: TemplateResult[] = [];
|
||||
|
||||
if (reviewers.length) {
|
||||
result.push(html`<div>${reviewers.map((u) => u.name).join(", ")}</div>`);
|
||||
}
|
||||
|
||||
const groupList = reviewerGroups.map((g) => g.name).join(", ");
|
||||
|
||||
const label =
|
||||
minReviewers === 1
|
||||
? reviewerGroups.length === 1
|
||||
? msg(str`At least ${minReviewers} user from this group: ${groupList}.`)
|
||||
: msg(str`At least ${minReviewers} user from these groups: ${groupList}.`)
|
||||
: reviewerGroups.length === 1
|
||||
? msg(str`At least ${minReviewers} users from this group: ${groupList}.`)
|
||||
: msg(str`At least ${minReviewers} users from these groups: ${groupList}.`);
|
||||
|
||||
result.push(html`<div>${label}</div>`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected renderOpenedOn(): SlottedTemplateResult {
|
||||
return html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Review opened on")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.openedOn}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderGracePeriodTill(): SlottedTemplateResult {
|
||||
return html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Grace period till")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.gracePeriodEnd}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderNextReviewDate(): SlottedTemplateResult {
|
||||
return html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Next review date")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.nextReviewDate}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderReviewDates() {
|
||||
return match(this.iteration?.state)
|
||||
.with(P.nullish, LifecycleIterationStateEnum.UnknownDefaultOpenApi, () => nothing)
|
||||
.with(
|
||||
LifecycleIterationStateEnum.Pending,
|
||||
() => html`${this.renderOpenedOn()}${this.renderGracePeriodTill()}`,
|
||||
)
|
||||
.with(LifecycleIterationStateEnum.Reviewed, () => this.renderNextReviewDate())
|
||||
.with(LifecycleIterationStateEnum.Overdue, () => this.renderOpenedOn())
|
||||
.with(LifecycleIterationStateEnum.Canceled, () => this.renderOpenedOn())
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
protected renderReviewSummary() {
|
||||
return html`<div class="pf-c-card pf-l-grid__item pf-m-3-col">
|
||||
<div class="pf-c-card__title">${msg("Latest review for this object")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Review state")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${LifecycleIterationStatus({
|
||||
status: this.iteration?.state,
|
||||
})}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Required reviewers")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${this.renderReviewers()}</div>
|
||||
</dd>
|
||||
</div>
|
||||
${this.renderReviewDates()}
|
||||
</dl>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Table
|
||||
|
||||
protected row(item: Review): SlottedTemplateResult[] {
|
||||
return [
|
||||
Timestamp(item.timestamp),
|
||||
html`<span>${item.reviewer.name}</span>`,
|
||||
html`<span>${item.note}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
protected override renderEmpty(): SlottedTemplateResult {
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state icon="pf-icon-task"
|
||||
><span>${this.emptyStateMessage}</span></ak-empty-state
|
||||
>`,
|
||||
);
|
||||
}
|
||||
|
||||
protected renderObjectCreate(): SlottedTemplateResult {
|
||||
if (!this.iteration?.userCanReview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ModalInvokerButton(ObjectReviewForm, {
|
||||
iteration: this.iteration,
|
||||
});
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
return html`<ak-lifecycle-preview-banner></ak-lifecycle-preview-banner>
|
||||
<div class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
${this.renderReviewSummary()}
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-9-col">
|
||||
<div class="pf-c-card__title">${msg("Reviews")}</div>
|
||||
${super.render()}
|
||||
</div>
|
||||
</div>
|
||||
return html`
|
||||
<ak-lifecycle-preview-banner></ak-lifecycle-preview-banner>
|
||||
<div class="pf-l-grid pf-m-gutter pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<h2 class="pf-c-title pf-m-xl">
|
||||
${this.iterations?.length
|
||||
? msg("The following reviews apply to this object:")
|
||||
: msg("This object has no reviews yet.")}
|
||||
</h2>
|
||||
${this.iterations?.map(
|
||||
(i) =>
|
||||
html` <h3 class="pf-c-title pf-m-lg">${i.rule.name}</h3>
|
||||
<ak-object-review-iteration
|
||||
.iteration=${i}
|
||||
class="pf-u-pl-lg-on-lg"
|
||||
></ak-object-review-iteration>`,
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
281
web/src/admin/lifecycle/ObjectReviewIteration.ts
Normal file
281
web/src/admin/lifecycle/ObjectReviewIteration.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import "#admin/lifecycle/LifecyclePreviewBanner";
|
||||
import "#components/ak-textarea-input";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/timestamp/ak-timestamp";
|
||||
import "#admin/lifecycle/ObjectReviewForm";
|
||||
|
||||
import { createPaginatedResponse } from "#common/api/responses";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { ModalInvokerButton } from "#elements/dialogs";
|
||||
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { ObjectReviewForm } from "#admin/lifecycle/ObjectReviewForm";
|
||||
import { LifecycleIterationStatus } from "#admin/lifecycle/utils";
|
||||
|
||||
import { LifecycleIteration, LifecycleIterationStateEnum, Review } from "@goauthentik/api";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
|
||||
@customElement("ak-object-review-iteration")
|
||||
export class ObjectReviewIteration extends Table<Review> {
|
||||
static styles = [
|
||||
// ---
|
||||
...super.styles,
|
||||
PFGrid,
|
||||
PFBanner,
|
||||
PFCard,
|
||||
PFFlex,
|
||||
PFDescriptionList,
|
||||
];
|
||||
|
||||
//#region Public Properties
|
||||
|
||||
@property({ attribute: false })
|
||||
public iteration: LifecycleIteration | null = null;
|
||||
|
||||
public override paginated = false;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Protected Properties
|
||||
|
||||
protected override emptyStateMessage = msg("No reviews yet.");
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
[msg("Reviewed on"), "timestamp"],
|
||||
[msg("Reviewer"), "reviewer"],
|
||||
[msg("Note"), "note"],
|
||||
];
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("iteration")) {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
protected apiEndpoint(): Promise<PaginatedResponse<Review>> {
|
||||
if (!this.iteration) {
|
||||
return Promise.resolve(createPaginatedResponse<Review>());
|
||||
}
|
||||
|
||||
return Promise.resolve(createPaginatedResponse(this.iteration.reviews));
|
||||
}
|
||||
|
||||
#triggerRefresh = () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
//#region Summary Card
|
||||
|
||||
protected renderReviewers(): SlottedTemplateResult {
|
||||
if (!this.iteration) {
|
||||
return html`<span>${msg("No review iteration found for this object.")}</span>`;
|
||||
}
|
||||
|
||||
const { reviewers, reviewerGroups, minReviewers } = this.iteration.rule;
|
||||
|
||||
const result: TemplateResult[] = [];
|
||||
|
||||
if (reviewers.length) {
|
||||
result.push(html` <div>${reviewers.map((u) => u.name).join(", ")}</div>`);
|
||||
}
|
||||
|
||||
const groupList = reviewerGroups.map((g) => g.name).join(", ");
|
||||
|
||||
const label =
|
||||
minReviewers === 1
|
||||
? reviewerGroups.length === 1
|
||||
? msg(str`At least ${minReviewers} user from this group: ${groupList}.`)
|
||||
: msg(str`At least ${minReviewers} user from these groups: ${groupList}.`)
|
||||
: reviewerGroups.length === 1
|
||||
? msg(str`At least ${minReviewers} users from this group: ${groupList}.`)
|
||||
: msg(str`At least ${minReviewers} users from these groups: ${groupList}.`);
|
||||
|
||||
result.push(html` <div>${label}</div>`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected renderOpenedOn(): SlottedTemplateResult {
|
||||
return html` <div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Review opened on")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.openedOn}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderGracePeriodTill(): SlottedTemplateResult {
|
||||
return html` <div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Grace period till")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.gracePeriodEnd}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderNextReviewDate(): SlottedTemplateResult {
|
||||
return html` <div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Next review date")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.iteration?.nextReviewDate}
|
||||
.elapsed=${false}
|
||||
dateonly
|
||||
datetime
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderReviewDates() {
|
||||
return match(this.iteration?.state)
|
||||
.with(P.nullish, LifecycleIterationStateEnum.UnknownDefaultOpenApi, () => nothing)
|
||||
.with(
|
||||
LifecycleIterationStateEnum.Pending,
|
||||
() => html`${this.renderOpenedOn()}${this.renderGracePeriodTill()}`,
|
||||
)
|
||||
.with(LifecycleIterationStateEnum.Reviewed, () => this.renderNextReviewDate())
|
||||
.with(LifecycleIterationStateEnum.Overdue, () => this.renderOpenedOn())
|
||||
.with(LifecycleIterationStateEnum.Canceled, () => this.renderOpenedOn())
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
protected renderReviewSummary() {
|
||||
return html` <div class="pf-c-card pf-l-grid__item pf-m-3-col">
|
||||
<div class="pf-c-card__title">${msg("Latest review for this object")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Review state")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${LifecycleIterationStatus({
|
||||
status: this.iteration?.state,
|
||||
})}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Required reviewers")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${this.renderReviewers()}</div>
|
||||
</dd>
|
||||
</div>
|
||||
${this.renderReviewDates()}
|
||||
</dl>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Table
|
||||
|
||||
protected renderToolbar(): SlottedTemplateResult {
|
||||
return html`${this.renderObjectCreate()}
|
||||
<ak-spinner-button .callAction=${this.#triggerRefresh} class="pf-m-secondary">
|
||||
${msg("Refresh")}
|
||||
</ak-spinner-button>`;
|
||||
}
|
||||
|
||||
protected row(item: Review): SlottedTemplateResult[] {
|
||||
return [
|
||||
Timestamp(item.timestamp),
|
||||
html`<span>${item.reviewer.name}</span>`,
|
||||
html`<span>${item.note}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
protected override renderEmpty(): SlottedTemplateResult {
|
||||
return super.renderEmpty(
|
||||
html` <ak-empty-state icon="pf-icon-task"
|
||||
><span>${this.emptyStateMessage}</span></ak-empty-state
|
||||
>`,
|
||||
);
|
||||
}
|
||||
|
||||
protected renderObjectCreate(): SlottedTemplateResult {
|
||||
if (!this.iteration?.userCanReview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ModalInvokerButton(ObjectReviewForm, {
|
||||
iteration: this.iteration,
|
||||
});
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
return html` <div class="pf-l-grid pf-m-gutter">
|
||||
${this.renderReviewSummary()}
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-9-col">
|
||||
<div class="pf-c-card__title">${msg("Reviews")}</div>
|
||||
<div class="pf-c-card__body">${super.render()}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-object-review-iteration": ObjectReviewIteration;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export class ReviewListPage extends TablePage<LifecycleIteration> {
|
||||
protected columns: TableColumn[] = [
|
||||
[msg("State"), "state"],
|
||||
[msg("Object"), "content_type__model"],
|
||||
[msg("Rule"), "rule__name"],
|
||||
[msg("Opened"), "opened_on"],
|
||||
[msg("Grace period ends")],
|
||||
];
|
||||
@@ -78,6 +79,7 @@ export class ReviewListPage extends TablePage<LifecycleIteration> {
|
||||
return [
|
||||
LifecycleIterationStatus({ status: item.state }),
|
||||
html`<a href="#${item.objectAdminUrl}">${item.objectVerbose}</a>`,
|
||||
html`${item.rule.name}`,
|
||||
html`<ak-timestamp .timestamp=${item.openedOn} datetime dateonly></ak-timestamp>`,
|
||||
html`<ak-timestamp .timestamp=${item.gracePeriodEnd} datetime dateonly></ak-timestamp>`,
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ export abstract class BaseStageForm<T extends Stage> extends ModelForm<T, string
|
||||
public static override verboseName = msg("Stage");
|
||||
public static override verboseNamePlural = msg("Stages");
|
||||
|
||||
getSuccessMessage(): string {
|
||||
public override getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated stage.")
|
||||
: msg("Successfully created stage.");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user