Compare commits
7 Commits
github-ci-
...
web/docs/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e79b4793 | ||
|
|
4f16bed6a5 | ||
|
|
8fb3569333 | ||
|
|
471493a118 | ||
|
|
8c27b7db26 | ||
|
|
cea8f1624a | ||
|
|
d48c3382dd |
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
@@ -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: ""
|
||||
@@ -70,14 +64,30 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2
|
||||
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # 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,9 +97,7 @@ 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
|
||||
@@ -97,7 +105,7 @@ runs:
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
corepack npm ci --prefix web
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -14,8 +14,6 @@ media
|
||||
# Node
|
||||
|
||||
node_modules
|
||||
corepack.tgz
|
||||
.corepack
|
||||
|
||||
.cspellcache
|
||||
cspell-report.*
|
||||
|
||||
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
|
||||
|
||||
@@ -1,73 +1,31 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.api.version_history import VersionHistoryViewSet
|
||||
from authentik.api.v3.urls import router
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.api.domains import DomainViewSet
|
||||
from authentik.tenants.api.tenants import TenantViewSet
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet], full=True) -> dict[str, Callable]:
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
"""Test Viewset"""
|
||||
|
||||
def test_attrs(self: TestModelViewSets) -> None:
|
||||
"""Test attributes we require on all viewsets"""
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
def test_ordering(self: TestModelViewSets) -> None:
|
||||
"""Test that all ordering fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
for ordering_field in test_viewset.ordering:
|
||||
with self.subTest(ordering_field):
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'ordering': ordering_field}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_search(self: TestModelViewSets) -> None:
|
||||
"""Test that search fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'search': generate_id()}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
cases = {
|
||||
"attrs": test_attrs,
|
||||
}
|
||||
if full:
|
||||
cases["ordering"] = test_ordering
|
||||
cases["search"] = test_search
|
||||
return cases
|
||||
return tester
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
||||
continue
|
||||
full = viewset not in [VersionHistoryViewSet, DomainViewSet, TenantViewSet]
|
||||
for test, case in viewset_tester_factory(viewset, full=full).items():
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}_{test}", case)
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
||||
|
||||
@@ -20,16 +20,11 @@ class TestBrands(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_flags = {}
|
||||
for flag in Flag.available(visibility="public"):
|
||||
self.default_flags[flag().key] = flag.get()
|
||||
Brand.objects.all().delete()
|
||||
|
||||
@property
|
||||
def default_flags(self) -> dict[str, object]:
|
||||
"""Get current public flags.
|
||||
|
||||
Some tests define temporary Flag subclasses, so this can't be cached in setUp.
|
||||
"""
|
||||
return {flag().key: flag.get() for flag in Flag.available(visibility="public")}
|
||||
|
||||
def test_current_brand(self):
|
||||
"""Test Current brand API"""
|
||||
brand = create_test_brand()
|
||||
|
||||
@@ -47,8 +47,7 @@ class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
||||
search_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app__name",
|
||||
"app__slug",
|
||||
"app",
|
||||
"attributes",
|
||||
]
|
||||
filterset_fields = [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block head %}
|
||||
<style data-id="static-styles">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url|iriencode|safe }}");
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||
background-image: url("{{ flow_background_url }}");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style data-id="flow-css">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"""stage view tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.models import Flow, FlowStageBinding
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
@@ -44,46 +42,6 @@ class TestViews(TestCase):
|
||||
"/static/dist/assets/images/flow_background.jpg",
|
||||
)
|
||||
|
||||
def test_flow_interface_css_background_preserves_presigned_url_query(self):
|
||||
"""Test flow CSS keeps signed URL query separators intact."""
|
||||
flow = create_test_flow()
|
||||
background_url = (
|
||||
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||
"&X-Amz-Signature=signature"
|
||||
)
|
||||
|
||||
with patch.object(Flow, "background_url", return_value=background_url):
|
||||
response = self.client.get(
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'--ak-global--background-image: url("{background_url}");',
|
||||
html=False,
|
||||
)
|
||||
|
||||
def test_flow_sfe_css_background_preserves_presigned_url_query(self):
|
||||
"""Test SFE flow CSS keeps signed URL query separators intact."""
|
||||
flow = create_test_flow()
|
||||
background_url = (
|
||||
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||
"&X-Amz-Signature=signature"
|
||||
)
|
||||
|
||||
with patch.object(Flow, "background_url", return_value=background_url):
|
||||
response = self.client.get(
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + "?sfe"
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'background-image: url("{background_url}");',
|
||||
html=False,
|
||||
)
|
||||
|
||||
|
||||
def view_tester_factory(view_class: type[StageView]) -> Callable:
|
||||
"""Test a form"""
|
||||
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-30 00:27+0000\n"
|
||||
"POT-Creation-Date: 2026-04-29 00:28+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -224,14 +224,6 @@ msgid ""
|
||||
"providers are returned. When set to false, backchannel providers are excluded"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "Invalid password hash format. Must be a valid Django password hash."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "Cannot set both password and password_hash. Use only one."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "No leading or trailing slashes allowed."
|
||||
msgstr ""
|
||||
|
||||
@@ -19,7 +19,6 @@ Forti
|
||||
Fortigate
|
||||
Gatus
|
||||
Gestionnaire
|
||||
ghec
|
||||
Gitea
|
||||
Gravitee
|
||||
Homarr
|
||||
|
||||
@@ -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
|
||||
@@ -83,8 +83,7 @@ pub(crate) fn start(tasks: &mut Tasks) -> Result<Arc<Metrics>> {
|
||||
"metrics",
|
||||
router.clone(),
|
||||
addr,
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev */
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -93,8 +92,7 @@ pub(crate) fn start(tasks: &mut Tasks) -> Result<Arc<Metrics>> {
|
||||
"metrics",
|
||||
router,
|
||||
unix::net::SocketAddr::from_pathname(socket_path())?,
|
||||
config::get().debug, /* Allow failure in case the server is running on the same machine,
|
||||
* like in dev */
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev
|
||||
)?;
|
||||
|
||||
Ok(metrics)
|
||||
|
||||
@@ -328,8 +328,8 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result<Arc<Workers>> {
|
||||
"worker",
|
||||
router.clone(),
|
||||
addr,
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev. */
|
||||
true, /* Allow failure in case the server is running on the same machine, like
|
||||
* in dev. */
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -338,8 +338,7 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result<Arc<Workers>> {
|
||||
"worker",
|
||||
router,
|
||||
unix::net::SocketAddr::from_pathname(socket_path())?,
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev. */
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev.
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
367
web/README.md
@@ -1,112 +1,303 @@
|
||||
# authentik WebUI
|
||||
# The authentik WebUI
|
||||
|
||||
This is the default UI for the authentik server. The documentation is going to be a little sparse
|
||||
for awhile, but at least let's get started.
|
||||
The authetik WebUI is the default UI for the authentik Single Sign-on (SSO) server. It consists of
|
||||
three primary applications:
|
||||
|
||||
# The Theory of the authentik UI
|
||||
- Flow: The transaction-driven, customizable interface for logging in and all other workflow
|
||||
activities
|
||||
- User: The user's library of applications to which they have access, and user settings such as
|
||||
configuring their MFA and email address, viewing their current sessions, and more
|
||||
- Admin: The system administration tool for defining applications, providers, policies, and
|
||||
everything else
|
||||
|
||||
In Peter Naur's 1985 essay [Programming as Theory
|
||||
Building](https://pages.cs.wisc.edu/~remzi/Naur.pdf), programming is described as creating a mental
|
||||
model of how a program _should_ run, then writing the code to test if the program _can_ run that
|
||||
way.
|
||||
Each of these is a [thin client] application around data objects provided by and transactions
|
||||
available with the authentik SSO server. Business logic and validation is provided by the server.
|
||||
|
||||
The mental model for the authentik UI is straightforward. There are five "applications" within the
|
||||
UI, each with its own base URL, router, and responsibilities, and each application needs as many as
|
||||
three contexts in which to run.
|
||||
The authentik SSO server is written in Python and Django.
|
||||
|
||||
The three contexts corresponds to objects in the API's `model` section, so let's use those names.
|
||||
> - [thin client](https://en.wikipedia.org/wiki/Thin_client): In this case, we mean "a front end to
|
||||
> show the data where the server does all the heavy lifting."
|
||||
|
||||
- The root `Config`. The root configuration object of the server, containing mostly caching and
|
||||
error reporting information. This is misleading, however; the `Config` object contains some user
|
||||
information, specifically a list of permissions the current user (or "no user") has.
|
||||
- The root `CurrentTenant`. This describes the `Brand` information UIs should use, such as themes,
|
||||
logos, favicon, and specific default flows for logging in, logging out, and recovering a user
|
||||
password.
|
||||
- The current `SessionUser`, the person logged in: username, display name, and various states.
|
||||
(Note: the authentik server permits administrators to "impersonate" any other user in order to
|
||||
debug their authentication experience. If impersonation is active, the `user` field reflects that
|
||||
user, but it also includes a field, `original`, with the administrator's information.)
|
||||
## Project setup
|
||||
|
||||
(There is a fourth context object, Version, but its use is limited to displaying version information
|
||||
and checking for upgrades. Just be aware that you will see it, but you will probably never interact
|
||||
with it.)
|
||||
If you have cloned the authentik repository, the [developer docs] are where you go to perform the
|
||||
initial set-up and install. This sequence will get you up and running in the usual course of events.
|
||||
|
||||
There are five applications. Two (`loading` and `api-browser`) are trivial applications whose
|
||||
insides are provided by third-party libraries (Patternfly and Rapidoc, respectively). The other
|
||||
three are actual applications. The descriptions below are wholly from the view of the user's
|
||||
experience:
|
||||
```
|
||||
$ make install
|
||||
$ make gen-dev-config
|
||||
$ make migrate
|
||||
$ make run-server
|
||||
$ make run-worker
|
||||
```
|
||||
|
||||
- `Flow`: From a given URL, displays a form that requests information from the user to accomplish a
|
||||
task. Some tasks require the user to be logged in, but many (such as logging in itself!)
|
||||
obviously do not.
|
||||
- `User`: Provides the user with access to the applications they can access, plus a few user
|
||||
settings.
|
||||
- `Admin`: Provides someone with super-user permissions access to the administrative functions of
|
||||
the authentik server.
|
||||
We recommend that you run `run-server` and `run-worker` in different terminals or different sessions
|
||||
under tmux or screen or a similar terminal multiplexer.
|
||||
|
||||
**Mental Model**
|
||||
The WebUI runs in this folder (`./web` under the project root). You can put the WebUI into hot
|
||||
reload by running, from this folder,
|
||||
|
||||
- Upon initialization, _every_ authentik UI application fetches `Config` and `CurrentTenant`. `User`
|
||||
and `Admin` will also attempt to load the `SessionUser`; if there is none, the user is kicked out
|
||||
to the `Flow` for logging into authentik itself.
|
||||
- `Config`, `CurrentTenant`, and `SessionUser`, are provided by the `@goauthentik/api` application,
|
||||
not by the codebase under `./web`. (Where you are now).
|
||||
- `Flow`, `User`, and `Admin` are all called `Interfaces` and are found in
|
||||
`./web/src/flow/FlowInterface`, `./web/src/user/UserInterface`, `./web/src/admin/AdminInterface`,
|
||||
respectively.
|
||||
```
|
||||
$ npm run watch
|
||||
```
|
||||
|
||||
Inside each of these you will find, in a hierarchal order:
|
||||
## Front-End Architecture
|
||||
|
||||
- The context layer described above
|
||||
- A theme managing layer
|
||||
- The orchestration layer:
|
||||
- web socket handler for server-generated events
|
||||
- The router
|
||||
- Individual routes for each vertical slice and its relationship to other objects:
|
||||
### The Django side
|
||||
|
||||
Each slice corresponds to an object table on the server, and each slice _usually_ consists of the
|
||||
following:
|
||||
The authentik web-based applications are delivered via a Django server. The server delivers an HTML
|
||||
template that executes a sequence of startup operations:
|
||||
|
||||
- A paginated collection display, usually using the `Table` foundation (found in
|
||||
`./web/src/elements/Table`)
|
||||
- The ability to view an individual object from the collection, which you may be able to:
|
||||
- Edit
|
||||
- Delete
|
||||
- A form for creating a new object
|
||||
- Tabs showing that object's relationship to other objects
|
||||
- Interactive elements for changing or deleting those relationships, or creating new ones.
|
||||
- The ability to create new objects with which to have that relationship, if they're not part of
|
||||
the core objects (such as User->MFA authenticator apps, since the latter is not a "core" object
|
||||
and has no tab of its own).
|
||||
- Assigns the language code and `data-theme="light|dark"` settings to the `html` tag. Assigns the
|
||||
favicon links. Creates a `window.authentik` object and assigns a variety of site-wide
|
||||
configuration details to it. Probes the a priority decision list of sources of the user's
|
||||
light/dark preferences and assigns that to the `Document.dataset`
|
||||
- Begins loading the interface root bundle. The script is of `type="module"`; it will not be
|
||||
executed until the initial HTML has been completely parsed.
|
||||
- Loads the site-wide standard CSS.
|
||||
- Injects any CSS overrides specified in the customer's `brand` settings
|
||||
- Loads any necessary JavaScript polyfills
|
||||
- Dispatches any initial messages to the notification handler
|
||||
- Sets any custom `<meta>` settings specified by the server
|
||||
- Provides the initial HTML scaffolding to launch an interface.
|
||||
|
||||
We are still a bit "all over the place" with respect to sub-units and common units; there are
|
||||
folders `common`, `elements`, and `components`, and ideally they would be:
|
||||
The interface code is mostly the core web component and its responsibilities. This code will be
|
||||
hydrated when the interface root bundle in the second step above is executed and the components are
|
||||
registered with the browser.
|
||||
|
||||
- `common`: non-UI related libraries all of our applications need
|
||||
- `elements`: UI elements shared among multiple applications that do not need context
|
||||
- `components`: UI elements shared among multiple that use one or more context
|
||||
### The Flow interface
|
||||
|
||||
... but at the moment there are some context-sensitive elements, and some UI-related stuff in
|
||||
`common`.
|
||||
The Flow interface has three subsystems:
|
||||
|
||||
# Comments
|
||||
- The Locale Selector: `<ak-locale-select>`
|
||||
- The Flow Inspector: `<ak-flow-inspector>`
|
||||
- The Flow Executor: `<ak-flow-executor>`
|
||||
|
||||
**NOTE:** The comments in this section are for specific changes to this repository that cannot be
|
||||
reliably documented any other way. For the most part, they contain comments related to custom
|
||||
settings in JSON files, which do not support comments.
|
||||
The Flow interface is a single-page application. The Locale Selector and the Inspector are
|
||||
independent buttons that persist on the web page for the duration of a Flow. Either can be disabled
|
||||
and hidden by admin preference. The Locale Selector allows the user to select an alternative locale
|
||||
in which to display text and labels. The [Flow
|
||||
Inspector](https://docs.goauthentik.io/add-secure-apps/flows-stages/flow/inspector/), when enabled,
|
||||
can query the server and then display details about the state of a flow: the accumulated context of
|
||||
the current flow, existing error messages, and expected next steps; it is present to assist with
|
||||
debugging.
|
||||
|
||||
- `tsconfig.json`:
|
||||
- `compilerOptions.useDefineForClassFields: false` is required to make TSC use the "classic" form
|
||||
of field definition when compiling class definitions. Storybook does not handle the ESNext
|
||||
proposed definition mechanism (yet).
|
||||
- `compilerOptions.plugins.ts-lit-plugin.rules.no-unknown-tag-name: "off"`: required to support
|
||||
rapidoc, which exports its tag late.
|
||||
- `compilerOptions.plugins.ts-lit-plugin.rules.no-missing-import: "off"`: lit-analyzer currently
|
||||
does not support path aliases very well, and cannot find the definition files associated with
|
||||
imports using them.
|
||||
- `compilerOptions.plugins.ts-lit-plugin.rules.no-incompatible-type-binding: "warn"`: lit-analyzer
|
||||
does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw
|
||||
too many errors to be supportable.
|
||||
The Executor is the heart of the system. It executes Flows.
|
||||
|
||||
A _Flow_ in authentik is the workflow that accomplishes a specific SSO-oriented task such as logging
|
||||
in, logging out, or enrolling as a new user, among others.
|
||||
|
||||
The Executor starts by examining the current URL for the `flowSlug`, and sends a request for a
|
||||
_Challenge_ to the server. Upon the response, the Executor loads the corresponding _Stage_: the UI
|
||||
component responsible for showing the challenge to the user. When the user performs the requested
|
||||
action the input is sent to the server, which issues a new Challenge. This Challenge may be the same
|
||||
one with error messages, or the next one in the workflow. This process repeats until the user
|
||||
reaches the end of the Flow, at which point the task is complete or failed.
|
||||
|
||||
The architecture for the Executor is straightforward:
|
||||
|
||||
- The HTML Document
|
||||
- The Locale Selector
|
||||
- The Inspector
|
||||
- The Executor
|
||||
- The current Stage
|
||||
|
||||
A Stage may have interior stages or components. The Identification Stage is the most complex of our
|
||||
stages. It usually shows the Username field, and in some configurations it _can_ show the Password
|
||||
field; in that case, the password component exists to allow the user to "show password". It may also
|
||||
host the Captcha and Passkey stages within, to complete the initial task of determining and
|
||||
validating a user's identity.
|
||||
|
||||
### User and Admin Interfaces
|
||||
|
||||
The architecture of these interfaces is more complex. In both cases, the user is assumed to have
|
||||
logged in and so is said to have a _Session_. The architecture is structured:
|
||||
|
||||
- The HTML Document
|
||||
- The Interface
|
||||
- License: a context handler for the site's enterprise license status
|
||||
- Session: a context handler for the user's current session. This mostly the `user` identity
|
||||
- Version: a context handler for the current version of authentik
|
||||
- Notifications: a context handler for outstanding messages sent from the server to the user
|
||||
- Capabilities: a list of features that the current user may use. List includes "can save
|
||||
reports," "can use debugging feature," "can use enterprise features."
|
||||
- The Application:
|
||||
- Header
|
||||
- Sidebar
|
||||
- Router
|
||||
- CRUD interfaces to features of the system:
|
||||
- Dashboard
|
||||
- Logs
|
||||
- Configurations
|
||||
- Flows, Stages & Policies
|
||||
- Users & Group
|
||||
- IDP Sources
|
||||
- Everything else!
|
||||
|
||||
### Miscellaneous interfaces
|
||||
|
||||
There are three miscellaneous interfaces:
|
||||
|
||||
#### API browser
|
||||
|
||||
A single page application that loads our schema and allows the user to experiment with it. Uses the
|
||||
[RapiDoc](https://rapidocweb.com/) app.
|
||||
|
||||
#### Loading
|
||||
|
||||
The Django application is wrapped in a proxy server for caching and performance; while it is in
|
||||
start-up mode, the proxy serves this page, which just says "The application is loading" with a
|
||||
pretty animation.
|
||||
|
||||
#### SFE: Simplified Flow Executor
|
||||
|
||||
The SFE is a limited version of the Flow Executor written to use [jQuery](https://jquery.com/). It
|
||||
supports only login operations, and is meant for places where the login is embedded in an Office365
|
||||
or MicrosoftTeams settings, as those use Trident (Internet Explorer) for their web-based login.
|
||||
|
||||
## Front-end foundations
|
||||
|
||||
### CSS
|
||||
|
||||
Our current CSS is provided by [Patternfly 4](https://v4-archive.patternfly.org/v4/). There are two
|
||||
different layers of CSS.
|
||||
|
||||
The first is the Global CSS that appears in the `<head>`. This defines the basic look: theme,
|
||||
start-up, reset, and fonts. It also provides the [CSS Custom
|
||||
Properties](https://docs.goauthentik.io/brands/custom-css/) that control the look and feel of the
|
||||
rest of an Interface.
|
||||
|
||||
The second is per-component CSS. This is linked into each component using [Adopted
|
||||
Stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets).
|
||||
|
||||
> This recipe has led to some significant awkwardness. Information from the outside does not pierce
|
||||
> the shadowDOM, so Patternfly-Base is linked into every component just to provide the box model,
|
||||
> reset, basic functionality, and behavioral modifiers. The interior of our components is cluttered
|
||||
> with lots of patternfly classes.
|
||||
|
||||
### Elements
|
||||
|
||||
Elements are custom web components that authentik has written that supply advanced behaviors to
|
||||
common operations, as well as API-independent complex components such as rich drop-downs, dual-pane
|
||||
selectors, toggles, switches, and wizards. At least, that's the idea. We are still untangling.
|
||||
|
||||
### Components
|
||||
|
||||
Components are custom web components that authentik has written that are API-aware and that supply
|
||||
business logic to perform validation, permissioning, and selective display.
|
||||
|
||||
## Adding a new feature (developer's guide)
|
||||
|
||||
As a thin client, the primary task will either be adding a new CRUD vertical or extending and
|
||||
enhancing an existing one. (If the elements, components, API, and so on represent the horizontal
|
||||
layers of an application, a single CRUD task is the "vertical slice" through these.) Our Django
|
||||
application presents collections of objects from which the user may pick one to view, update, or
|
||||
delete.
|
||||
|
||||
The web component in `./elements/table` is used to display, well, tables of components. A new
|
||||
feature begins by inheriting the `Table` class and providing two things: the API call to retrieve
|
||||
the objects, and a method describing a row for the table. This is the retrieval for our Role-Based
|
||||
Access Controls (RBAC).
|
||||
|
||||
```
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Role>> {
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacRolesList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
managedIsnull: this.hideManaged ? true : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The complete list of APIs available can be found in `node_modules/@goauthentik/api/src/apis`.
|
||||
|
||||
A row returns an array of cells:
|
||||
|
||||
```
|
||||
row(item: Role): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update Role")}</span>
|
||||
<ak-role-form slot="form" .instancePk=${item.pk}> </ak-role-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This example shows the use of the `modal` dialogue to show the "update role" form. Deciding to use
|
||||
a modal or to move to a different page is a matter of taste, but mostly rests on how large the form
|
||||
is. If it's likely to have internal scrolling, opt for a separate page.
|
||||
|
||||
For complex objects that have a lot of detail or subsidiary lists of features (such as Flows),
|
||||
provide a separate View page for each one. We have a specified display standard encapsulated in our
|
||||
`DictionaryList` component.
|
||||
|
||||
Creation and Updating are handled using the web component parent in `./elements/forms`. Like
|
||||
tables, a child component inherits and extends the Form class, providing three features: how to
|
||||
_retrieve_ the object, how to _send_ the object, and what to ask for. (RBAC is small enough, it's
|
||||
useful as an example):
|
||||
|
||||
```
|
||||
loadInstance(pk: string): Promise<Role> {
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacRolesRetrieve({
|
||||
uuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: Role): Promise<Role> {
|
||||
if (this.instance?.pk) {
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacRolesPartialUpdate({
|
||||
uuid: this.instance.pk,
|
||||
patchedRoleRequest: data,
|
||||
});
|
||||
}
|
||||
return new RbacApi(DEFAULT_CONFIG).rbacRolesCreate({
|
||||
roleRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The `send` shows two different modes: If the existing instance has an identity, this is an update;
|
||||
otherwise it's a creation request.
|
||||
|
||||
These are _simple_ examples, naturally, and our application can get much more complicated. The
|
||||
`./admin/flows` vertical is one of the most complex, including:
|
||||
|
||||
- A per-flow view page with a [Mermaid](https://mermaid.js.org/) diagram to show a Flow's Stages
|
||||
- A sub-table of the Flow's Policies, with the ability to edit each Policy or its Bindings
|
||||
- A sub-table of the Flow's Stages with the ability to edit each Stage or a Stage's Binding directly
|
||||
- A sub-table of the Flow's Permissions
|
||||
|
||||
## Choosing To Use A Custom Component (developer's guide)
|
||||
|
||||
Some of our server-side objects come with lists. When editing a list, we suggest:
|
||||
|
||||
- If it's a simple list and there's only one choice, use `<select>`
|
||||
- If it's from the server and it's possible there are more than 100 items, use SearchSelect. It
|
||||
has features for showing complex list objects and narrowing down search items.
|
||||
- If the user can select multiple choices, use DualSelect
|
||||
|
||||
### License
|
||||
|
||||
|
||||
30
web/docs/2026-05-01-01-Foundations-Language.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 01 Foundations: Language
|
||||
|
||||
Date: 2026-05-01 (May 1st, 2026)
|
||||
|
||||
## The web application
|
||||
|
||||
The authentik web-based front-end is written in Typescript. We are currently targeting Typescript
|
||||
7.0, aka "TSGO," for its speed and compatibility. We chose Typescript because our experience has
|
||||
been that the type system, when used reliably, can prevent a wide class of errors, especially when
|
||||
negotiating with the authentik API as generated by OpenAPI.
|
||||
|
||||
- `mode: strict` is non-negotiable.
|
||||
- `useDefineForClassFields` is required for Lit decorator compatibility. See the Lit documentation
|
||||
[Typescript class fields for reactive
|
||||
properties](https://lit.dev/docs/components/properties/#:~:text=Set%20the%20useDefineForClassFields%20compiler%20option%20to%20false)
|
||||
for details.
|
||||
|
||||
## Tooling
|
||||
|
||||
Most of our internal tooling in the `./web/scripts` folder is written in JavaScript, not Typescript,
|
||||
to avoid the chicken-and-egg problem of needing build scripts to build build scripts. To facilitate
|
||||
checking, we enable `checkJs` and [jsdoc supported
|
||||
types](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#param-and-returns) in
|
||||
our tooling and script files, and we check them rigorously.
|
||||
|
||||
## Guidance:
|
||||
|
||||
Whenever tempted to use `any`, use `unknown` and a [type
|
||||
predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
|
||||
(sometimes called a "type guard") instead.
|
||||
27
web/docs/2026-05-01-02-Foundations-Build-Tools.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 02 Foundations: Build Tools
|
||||
|
||||
Date: 2026-05-01 (May 1st, 2026)
|
||||
|
||||
## Esbuild and TSGo
|
||||
|
||||
In 2024, the web UI used Rollup and TSC as its primary build tools. Building the entire UI for
|
||||
release took as many as three minutes.
|
||||
|
||||
ESBuild can both produce running Javascript from Typescript, and perform all of the bundling
|
||||
required to support the authentik WebUI. Switching to ESBuild reduced build time to 5 _seconds_. TSC
|
||||
has been relegated to the `no-emit` strategy of type-checking but not code-producing.
|
||||
|
||||
One complication in our code is that our web component foundation, Lit, has an awkward
|
||||
CSS-in-Javascript format incompatible with the build tools intended to support React, and the
|
||||
ESBuild plug-in to handle it is custom.
|
||||
|
||||
As of this writing, Typescript 7.0, aka "TSGo," is currently in beta. When it is released, we expect
|
||||
to both reassess this strategy and examine alternative build strategies. We prefer to hew as close
|
||||
to the Typescript standard as possible, and the standard is set by the Typescript team.
|
||||
|
||||
## Wireit
|
||||
|
||||
We have chosen to use Wireit because it provides a finer degree of control over build order and
|
||||
provides a caching strategy. This significantly speeds up rebuilding during development versus using
|
||||
NPM's own builds. Use Wireit _only_ when you need the cache or dependency order to be strict; for
|
||||
baseline builds, prefer writing directly into the `scripts` section of `package.json`.
|
||||
20
web/docs/2026-05-01-03-Foundations-Workspaces.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 01 Foundations: Workspaces
|
||||
|
||||
Date: 2026-05-02 (May 2st, 2026)
|
||||
|
||||
## Workspaces
|
||||
|
||||
In order to promote the use and development of a product by the widest community possible, we
|
||||
default to using NPM workspaces, since it is the most common too possible.
|
||||
|
||||
Provide a separate workspace when:
|
||||
|
||||
1. The project is support that applies across multiple other workspaces, rather than being a part of
|
||||
an application directly. `./packages/core` is the example.
|
||||
2. The project is a polyfill or library that is needed across all the applications supported by the
|
||||
front-end. `./packages/formdata-polyfill` is the example.
|
||||
|
||||
3. The project is an application that has radically different requirements from the standard set of
|
||||
applications. `./packages/sfe` exists to support only the Login Flow with the Internet Explorer
|
||||
11-based rendering engine, which is still embedded in some older Microsoft products we cannot
|
||||
afford to ignore.
|
||||
12
web/docs/2026-05-02-04-Foundation-Import-Strategies.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 01 Foundations: Import Strategies
|
||||
|
||||
Date: 2026-05-02 (May 2st, 2026)
|
||||
|
||||
## Import Strategies
|
||||
|
||||
`package.json` defines a large number of import paths that reach into the `src` folder. We use
|
||||
NodeJS subpaths prefixed with `#`, such as `#fonts`, `#elements`, or `#flow` to isolate subsections
|
||||
of the frontend. This strategy is intended to facilitate directory restructuring without having to
|
||||
do mass search-and-replace ops, and as a precursor to further mono-repo-ifying the codebase.
|
||||
|
||||
We recommend using barrel files only to export the intended API of a defined subsection.
|
||||
69
web/docs/2026-05-02-05-Foundation-Code-Quality.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 01 Foundations: Code Quality
|
||||
|
||||
Date: 2026-05-02 (May 2st, 2026)
|
||||
|
||||
## Code Linting
|
||||
|
||||
We _like_ our guardrails. We use ESLint with as many plug-ins as we can reasonably stuff into it for
|
||||
our checks, such as `eslint-plugin-lit` and `eslint-plugin-wc`, plus `lit-analyzer`.
|
||||
|
||||
## Code Formatting
|
||||
|
||||
We use `prettier` to enforce a coding style and to catch some fundamental syntax errors. The current
|
||||
`prettier` configuration correctly formats Lit's HTML-in-JS and CSS-it-JS use cases, as well as all
|
||||
the Typescript we can throw at it.
|
||||
|
||||
## Lockfile
|
||||
|
||||
We have a custom script in `./scripts/lint-lockfile.sh` that checks to ensure that every packages as
|
||||
a resolved hash.
|
||||
|
||||
## Type Checking
|
||||
|
||||
Although we use ESBuild to convert and bundle our Typescript into JavaScript, we use the stock
|
||||
Typescript compiler, `tsc`, to check our types. We maintain a default configuration with `use
|
||||
strict`.
|
||||
|
||||
## Testing
|
||||
|
||||
We do have tests, but they are primitive. We are very much in a move-fast and try hard not to break
|
||||
things. We strongly recommend that every PR include a description of how a peer would test the
|
||||
product manually to validate that it does what the PR claims it does.
|
||||
|
||||
Adding to the library of end-to-end tests is a critical mission.
|
||||
|
||||
## Your eyes, and the eyes of your peers
|
||||
|
||||
For all that we do like our guardrails, nothing surpasses peer review.
|
||||
|
||||
## AI Review
|
||||
|
||||
We have had mixed results using AI tools such as Claude and Copilot to vet our code. Claude,
|
||||
especially, can be very good at pointing out shortcomings and missed opportunities in a pull
|
||||
request, but it can also generate a lot of false positives or trivial issues. We recommend reading
|
||||
AI reviews with caution.
|
||||
|
||||
## AI Review Strategy
|
||||
|
||||
That said, this is the current template for a code review prompt. Start with the _target_ branch,
|
||||
then download the patch file into the project root. You can easily download the patch file by
|
||||
navigating to the Github PR and appending `.patch` to it, for example:
|
||||
`https://github.com/goauthentik/authentik/pull/21868.patch`
|
||||
|
||||
We use this template:
|
||||
|
||||
> Keep the tone neutral-professional-skeptical, the voice of an expert. Avoid excessive enthusiasm.
|
||||
> This is the root folder for the authentik single sign-on server.
|
||||
>
|
||||
> Read the patch file `./21868.patch`. This [community-provided] patch [describe the patch here in
|
||||
> your own words, using only one or two sentences].
|
||||
>
|
||||
> Task 1: Provide a high-level summary of the effect of applying `./21868.patch` Point out any
|
||||
> shortcomings or security considerations.
|
||||
>
|
||||
> Task2: If no tests are provided in the patch, describe how these changes could be tested.
|
||||
|
||||
Edit the patch number, add or remove "community-provided" as needed, and include your best
|
||||
understanding of what the patch claims to do in the second paragraph. Having a strong template that
|
||||
you hand-edit before running seems to work much better than using a generic template in a Claude
|
||||
skill.
|
||||
@@ -77,8 +77,6 @@ export class FormFixture extends PageFixture {
|
||||
|
||||
/**
|
||||
* Search for a row containing the given text.
|
||||
*
|
||||
* @returns A locator for the row entry matching the query.
|
||||
*/
|
||||
public search = async (
|
||||
query: string,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
|
||||
import { PageFixture, PageFixtureInit } from "#e2e/fixtures/PageFixture";
|
||||
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export const GOOD_USERNAME = "test-admin@goauthentik.io";
|
||||
export const GOOD_PASSWORD = "test-runner";
|
||||
|
||||
@@ -13,8 +11,6 @@ export interface LoginInit {
|
||||
username?: string;
|
||||
password?: string;
|
||||
to?: URL | string;
|
||||
rememberMe?: boolean;
|
||||
page?: Page;
|
||||
}
|
||||
|
||||
export interface SessionFixtureInit extends PageFixtureInit {
|
||||
@@ -40,10 +36,6 @@ export class SessionFixture extends PageFixture {
|
||||
public $passwordStage = this.page.locator("ak-stage-password");
|
||||
public $passwordField = this.page.getByLabel("Password");
|
||||
|
||||
public $rememberMeCheckbox = this.page.getByRole("checkbox", {
|
||||
name: "Remember me on this device",
|
||||
});
|
||||
|
||||
/**
|
||||
* The button to submit the the login flow,
|
||||
* typically redirecting to the authenticated interface.
|
||||
@@ -74,45 +66,19 @@ export class SessionFixture extends PageFixture {
|
||||
/**
|
||||
* Log into the application.
|
||||
*/
|
||||
public async login(
|
||||
{
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
rememberMe,
|
||||
}: LoginInit = {},
|
||||
page = this.page,
|
||||
): Promise<void> {
|
||||
public async login({
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
}: LoginInit = {}) {
|
||||
this.logger.info("Logging in...");
|
||||
|
||||
const initialURL = new URL(page.url());
|
||||
const initialURL = new URL(this.page.url());
|
||||
|
||||
if (initialURL.pathname === SessionFixture.pathname) {
|
||||
this.logger.info("Skipping navigation because we're already in a authentication flow");
|
||||
} else {
|
||||
await page.goto(to.toString());
|
||||
}
|
||||
|
||||
if (typeof rememberMe === "boolean") {
|
||||
const rememberMeCheckboxVisible = await this.$rememberMeCheckbox.isVisible();
|
||||
|
||||
if (rememberMeCheckboxVisible) {
|
||||
if (rememberMe) {
|
||||
await this.$rememberMeCheckbox.check();
|
||||
|
||||
await expect(
|
||||
this.$rememberMeCheckbox,
|
||||
"Remember me checkbox is checked",
|
||||
).toBeChecked();
|
||||
} else {
|
||||
await this.$rememberMeCheckbox.uncheck();
|
||||
|
||||
await expect(
|
||||
this.$rememberMeCheckbox,
|
||||
"Remember me checkbox is unchecked",
|
||||
).not.toBeChecked();
|
||||
}
|
||||
}
|
||||
await this.page.goto(to.toString());
|
||||
}
|
||||
|
||||
await this.$usernameField.fill(username);
|
||||
@@ -136,7 +102,7 @@ export class SessionFixture extends PageFixture {
|
||||
|
||||
//#region Navigation
|
||||
|
||||
public async toLoginPage(page: Page = this.page) {
|
||||
await page.goto(SessionFixture.pathname);
|
||||
public async toLoginPage() {
|
||||
await this.page.goto(SessionFixture.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
1884
web/package-lock.json
generated
@@ -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",
|
||||
@@ -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
@@ -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
|
||||
@@ -2,30 +2,20 @@ import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { FocusTarget } from "#elements/utils/focus";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { CoreApi, UserPasswordSetRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-user-password-form")
|
||||
export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
public static shadowRootOptions: ShadowRootInit = {
|
||||
...Form.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public static override verboseName = msg("Password");
|
||||
public static override verboseNamePlural = msg("Passwords");
|
||||
public static override submittingVerb = msg("Setting");
|
||||
public override submitLabel = msg("Set Password");
|
||||
|
||||
protected autofocusTarget = new FocusTarget<HTMLInputElement>();
|
||||
|
||||
@@ -33,9 +23,6 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
|
||||
//#region Properties
|
||||
|
||||
public override submitLabel = msg("Set Password");
|
||||
public override successMessage = msg("Successfully updated password.");
|
||||
|
||||
@property({ type: Number })
|
||||
public instancePk?: number;
|
||||
|
||||
@@ -43,15 +30,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
public label = msg("New Password");
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = msg("Type a new password...");
|
||||
public placeholder = msg("New Password");
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
public username: string | null = null;
|
||||
@property({ type: String })
|
||||
public username?: string;
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
public email: string | null = null;
|
||||
|
||||
public override size = PFSize.Medium;
|
||||
@property({ type: String })
|
||||
public email?: string;
|
||||
|
||||
/**
|
||||
* The autocomplete attribute to use for the password field.
|
||||
@@ -65,15 +50,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
|
||||
//#endregion
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully updated password.");
|
||||
}
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("focus", this.autofocusTarget.toEventListener());
|
||||
}
|
||||
|
||||
public override firstUpdated(): void {
|
||||
requestAnimationFrame(() => {
|
||||
this.focus();
|
||||
});
|
||||
this.focus();
|
||||
}
|
||||
|
||||
protected override async send(data: UserPasswordSetRequest): Promise<void> {
|
||||
@@ -107,26 +94,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
/>`
|
||||
: nothing}
|
||||
|
||||
<ak-form-element-horizontal required name="password">
|
||||
${AKLabel(
|
||||
{
|
||||
slot: "label",
|
||||
className: "pf-c-form__group-label",
|
||||
htmlFor: "password",
|
||||
required: true,
|
||||
},
|
||||
this.label,
|
||||
)}
|
||||
<ak-form-element-horizontal label=${this.label} required name="password">
|
||||
<input
|
||||
autofocus
|
||||
${this.autofocusTarget.toRef()}
|
||||
id="password"
|
||||
type="password"
|
||||
value=""
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
placeholder=${ifPresent(this.placeholder || this.label)}
|
||||
autocomplete=${ifPresent(this.autocomplete)}
|
||||
placeholder=${ifDefined(this.placeholder || this.label)}
|
||||
aria-label=${this.label}
|
||||
autocomplete=${ifDefined(this.autocomplete)}
|
||||
/>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* @file Storage utilities.
|
||||
*/
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
/**
|
||||
* A utility class for safely accessing web storage (localStorage or sessionStorage) with error handling.
|
||||
*/
|
||||
export class StorageAccessor {
|
||||
constructor(
|
||||
/**
|
||||
* The key under which the value is stored in the storage backend.
|
||||
*/
|
||||
public readonly key: string,
|
||||
/**
|
||||
* The storage backend to use, e.g. `window.localStorage` or `window.sessionStorage`.
|
||||
*/
|
||||
protected readonly storage: Storage,
|
||||
protected logger = ConsoleLogger.prefix("storage-accessor"),
|
||||
) {
|
||||
if (typeof key !== "string") {
|
||||
throw new TypeError("Storage key must be a string");
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new TypeError("Storage key must be a non-empty string");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StorageAccessor} for local storage.
|
||||
*
|
||||
* @param key The key under which the value is stored in localStorage.
|
||||
*/
|
||||
public static local = (key: string) => new StorageAccessor(key, self.localStorage);
|
||||
/**
|
||||
* Create a {@link StorageAccessor} for session storage.
|
||||
*
|
||||
* @param key The key under which the value is stored in sessionStorage.
|
||||
*/
|
||||
public static session = (key: string) => new StorageAccessor(key, self.sessionStorage);
|
||||
|
||||
/**
|
||||
* Read the value from storage.
|
||||
*
|
||||
* @param fallback An optional value to return if the key does not exist or an error occurs. Defaults to `null`.
|
||||
*
|
||||
* @returns The stored value, or `null` if the key does not exist or an error occurs.
|
||||
*/
|
||||
public read<T extends string>(fallback?: T): T | null {
|
||||
try {
|
||||
const value = this.storage.getItem(this.key);
|
||||
return value !== null ? (value as T) : (fallback ?? null);
|
||||
} catch (_error: unknown) {
|
||||
return fallback ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to storage.
|
||||
*
|
||||
* @param value The value to store.
|
||||
*
|
||||
* @returns `true` if the value was successfully stored, or `false` if an error occurred.
|
||||
*/
|
||||
public write(value: string | null): boolean {
|
||||
if (!value) {
|
||||
if (this.read()) {
|
||||
return this.delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
this.storage.setItem(this.key, value);
|
||||
return true;
|
||||
} catch (_error: unknown) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the value from storage and parse it as JSON.
|
||||
*
|
||||
* @param fallback An optional value to return if the key does not exist, the value is not valid JSON, or an error occurs. Defaults to `null`.
|
||||
*
|
||||
* @returns The parsed value, or `null` if the key does not exist, the value is not valid JSON, or an error occurs.
|
||||
*/
|
||||
public readJSON<T>(fallback?: T): T | null {
|
||||
const value = this.read<string>();
|
||||
|
||||
if (value === null) {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch (_error: unknown) {
|
||||
return fallback ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to storage after stringifying it as JSON.
|
||||
*
|
||||
* @param value The value to store.
|
||||
*
|
||||
* @returns `true` if the value was successfully stored, or `false` if an error occurred.
|
||||
*/
|
||||
public writeJSON(value: unknown): boolean {
|
||||
try {
|
||||
const stringified = JSON.stringify(value);
|
||||
return this.write(stringified);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to write JSON value to storage", error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the value from storage.
|
||||
*
|
||||
* @returns `true` if the value was successfully deleted, or `false` if an error occurred.
|
||||
*/
|
||||
public delete(): boolean {
|
||||
this.logger.debug("Deleting value from storage");
|
||||
|
||||
try {
|
||||
this.storage.removeItem(this.key);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to delete value from storage", error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,6 @@ export class NavigationButtons extends WithNotifications(WithSession(AKElement))
|
||||
<a
|
||||
href="${globalAK().api.base}flows/-/default/invalidation/"
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg("Sign out")}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Sign out")}>
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||
|
||||
@@ -34,7 +34,6 @@ export const MDXAnchor = ({
|
||||
const nextURL = new URL(nextPathname, import.meta.env.AK_DOCS_URL);
|
||||
// Remove trailing .md and .mdx, and trailing "index".
|
||||
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
href = nextURL.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -414,13 +414,6 @@ export class Form<T = Record<string, unknown>, D = T>
|
||||
|
||||
const { submittingVerb, verboseName } = this.constructor as typeof Form;
|
||||
|
||||
if (!verboseName) {
|
||||
return msg(str`${submittingVerb}...`, {
|
||||
id: "form.submitting.no-entity",
|
||||
desc: "The message shown while a form is being submitted, when no entity name is provided.",
|
||||
});
|
||||
}
|
||||
|
||||
return msg(str`${submittingVerb} ${verboseName}...`, {
|
||||
id: "form.submitting",
|
||||
desc: "The message shown while a form is being submitted.",
|
||||
@@ -622,7 +615,6 @@ export class Form<T = Record<string, unknown>, D = T>
|
||||
protected doSubmit = (event: SubmitEvent): void => {
|
||||
if (this.submitting) {
|
||||
this.logger.info("Skipping submit. Already submitting!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
@@ -4,44 +4,6 @@
|
||||
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
|
||||
export interface FocusErrorOptions extends ErrorOptions {
|
||||
target: Element | null;
|
||||
}
|
||||
|
||||
export class FocusAssertionError extends Error {
|
||||
public override name = "FocusAssertionError";
|
||||
public readonly target: Element | null;
|
||||
|
||||
constructor(message: string, { target, ...options }: FocusErrorOptions) {
|
||||
super(message, options);
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertFocusable(target: Element | null | undefined): asserts target is HTMLElement {
|
||||
if (!target) {
|
||||
throw new FocusAssertionError("Skipping focus, no target", { target: null });
|
||||
}
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new FocusAssertionError("Skipping focus, target is not an HTMLElement", { target });
|
||||
}
|
||||
|
||||
if (document.activeElement === target) {
|
||||
throw new FocusAssertionError("Target is already focused", { target });
|
||||
}
|
||||
|
||||
// Despite our type definitions, this method isn't available in all browsers,
|
||||
// so we fallback to assuming the element is visible.
|
||||
const visible = target.checkVisibility?.() ?? true;
|
||||
|
||||
if (!visible) {
|
||||
throw new FocusAssertionError("Skipping focus, target is not visible", { target });
|
||||
}
|
||||
|
||||
if (typeof target.focus !== "function") {
|
||||
throw new FocusAssertionError("Skipping focus, target has no focus method", { target });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Recursively check if the target element or any of its children are active (i.e. "focused").
|
||||
*
|
||||
@@ -74,17 +36,35 @@ export function isActiveElement(
|
||||
* @category DOM
|
||||
*/
|
||||
export function isFocusable(target: Element | null | undefined): target is HTMLElement {
|
||||
try {
|
||||
assertFocusable(target);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof FocusAssertionError) {
|
||||
console.debug(error.message, error.target);
|
||||
} else {
|
||||
console.error("Unexpected error during focus assertion", error);
|
||||
}
|
||||
if (!target) {
|
||||
console.debug("FocusTarget: Skipping focus, no target", target);
|
||||
return false;
|
||||
}
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
console.debug("FocusTarget: Skipping focus, target is not an HTMLElement", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.activeElement === target) {
|
||||
console.debug("FocusTarget: Target is already focused", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Despite our type definitions, this method isn't available in all browsers,
|
||||
// so we fallback to assuming the element is visible.
|
||||
const visible = target.checkVisibility?.() ?? true;
|
||||
|
||||
if (!visible) {
|
||||
console.debug("FocusTarget: Skipping focus, target is not visible", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof target.focus !== "function") {
|
||||
console.debug("FocusTarget: Skipping focus, target has no focus method", target);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ifPresent } from "#elements/utils/attributes";
|
||||
import { isDefaultAvatar } from "#elements/utils/images";
|
||||
|
||||
import Styles from "#flow/FormStatic.css";
|
||||
import { RememberMeStorage } from "#flow/stages/identification/controllers/RememberMeController";
|
||||
import { StageChallengeLike } from "#flow/types";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -70,9 +69,7 @@ export const FlowUserDetails: LitFC<FlowUserDetailsProps> = ({ challenge }) => {
|
||||
${flowInfo?.cancelUrl
|
||||
? html`
|
||||
<div slot="link">
|
||||
<a href=${flowInfo.cancelUrl} @click=${RememberMeStorage.reset}
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
<a href=${flowInfo.cancelUrl}>${msg("Not you?")}</a>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -121,10 +121,9 @@ export class InputPassword extends AKElement {
|
||||
|
||||
//#region Refs
|
||||
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public inputRef: Ref<HTMLInputElement> = createRef();
|
||||
inputRef: Ref<HTMLInputElement> = createRef();
|
||||
|
||||
public toggleVisibilityRef = createRef<HTMLButtonElement>();
|
||||
toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export abstract class BaseStage<Tin extends StageChallengeLike, Tout = unknown>
|
||||
@intersectionObserver()
|
||||
public visible = false;
|
||||
|
||||
protected autofocusTarget = new FocusTarget<HTMLInputElement>();
|
||||
protected autofocusTarget = new FocusTarget();
|
||||
focus = this.autofocusTarget.focus;
|
||||
|
||||
#visibilityListener = () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AKLabel } from "#components/ak-label";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import AutoRedirect from "#flow/stages/identification/controllers/AutoRedirectController";
|
||||
import CaptchaDisplayController from "#flow/stages/identification/controllers/CaptchaDisplayController";
|
||||
import RememberMeController from "#flow/stages/identification/controllers/RememberMeController";
|
||||
import RememberMe from "#flow/stages/identification/controllers/RememberMeController";
|
||||
import WebauthnController from "#flow/stages/identification/controllers/WebauthnController";
|
||||
import Styles from "#flow/stages/identification/styles.css";
|
||||
|
||||
@@ -30,7 +30,6 @@ import { match } from "ts-pattern";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, nothing, PropertyValues, ReactiveControllerHost } from "lit";
|
||||
import { createRef, ref } from "lit-html/directives/ref.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
|
||||
@@ -46,6 +45,8 @@ type IdentificationFooter = Partial<Pick<IdentificationChallenge, "enrollUrl" |
|
||||
|
||||
export type IdentificationHost = IdentificationStage & ReactiveControllerHost;
|
||||
|
||||
type EmptyString = string | null | undefined;
|
||||
|
||||
export const PasswordManagerPrefill: {
|
||||
password?: string;
|
||||
totp?: string;
|
||||
@@ -81,26 +82,21 @@ export class IdentificationStage extends BaseStage<
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
...RememberMeController.styles,
|
||||
...RememberMe.styles,
|
||||
Styles,
|
||||
];
|
||||
|
||||
/**
|
||||
* The ID of the identifier input field, used for accessibility and focus management.
|
||||
* The ID of the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
public inputID = "ak-identifier-input";
|
||||
|
||||
protected passwordFieldRef = createRef<HTMLInputElement>();
|
||||
|
||||
#form?: HTMLFormElement;
|
||||
|
||||
public defaultUserIdentification: string | null = null;
|
||||
|
||||
protected rememberMeController: RememberMeController | null = null;
|
||||
|
||||
private rememberMe = new RememberMe(this);
|
||||
#autoRedirect = new AutoRedirect(this);
|
||||
#captcha = new CaptchaDisplayController(this);
|
||||
#webauthn = new WebauthnController(this);
|
||||
@@ -113,23 +109,15 @@ export class IdentificationStage extends BaseStage<
|
||||
super();
|
||||
// We _define and instantiate_ these fields above, then _read_ them here, and that satisfies
|
||||
// the lint pass that there are no unused private fields.
|
||||
this.addController(this.rememberMe);
|
||||
this.addController(this.#autoRedirect);
|
||||
this.addController(this.#captcha);
|
||||
this.addController(this.#webauthn);
|
||||
}
|
||||
|
||||
#prepareRememberMeFrame = -1;
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("challenge") && this.challenge) {
|
||||
cancelAnimationFrame(this.#prepareRememberMeFrame);
|
||||
|
||||
this.#prepareRememberMeFrame = requestAnimationFrame(() => {
|
||||
this.prepareRememberMeController();
|
||||
});
|
||||
|
||||
this.#createHelperForm();
|
||||
}
|
||||
}
|
||||
@@ -139,46 +127,10 @@ export class IdentificationStage extends BaseStage<
|
||||
this.addEventListener("focus", this.autofocusTarget.toEventListener());
|
||||
}
|
||||
|
||||
public override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
cancelAnimationFrame(this.#prepareRememberMeFrame);
|
||||
}
|
||||
|
||||
public override firstUpdated(): void {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
protected prepareRememberMeController(): void {
|
||||
if (!this.challenge) return;
|
||||
|
||||
const { enableRememberMe, pendingUserIdentifier = null } = this.challenge;
|
||||
|
||||
if (!enableRememberMe) {
|
||||
this.defaultUserIdentification = pendingUserIdentifier;
|
||||
|
||||
if (this.rememberMeController) {
|
||||
this.removeController(this.rememberMeController);
|
||||
this.rememberMeController = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.rememberMeController) {
|
||||
this.rememberMeController = new RememberMeController(this, {
|
||||
identificationFieldID: this.inputID,
|
||||
identificationFieldRef: this.autofocusTarget.reference,
|
||||
passwordFieldRef: this.passwordFieldRef,
|
||||
pendingUserIdentifier,
|
||||
});
|
||||
|
||||
this.addController(this.rememberMeController);
|
||||
}
|
||||
|
||||
this.defaultUserIdentification = this.rememberMeController.defaultUserIdentification;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Helper Form
|
||||
@@ -295,11 +247,11 @@ export class IdentificationStage extends BaseStage<
|
||||
id: string,
|
||||
type: string,
|
||||
label: string,
|
||||
initialUserIdentification: string | null,
|
||||
username: EmptyString,
|
||||
autocomplete: string,
|
||||
) {
|
||||
return html`<input
|
||||
${ref(this.autofocusTarget.reference)}
|
||||
${this.autofocusTarget.toRef()}
|
||||
id=${id}
|
||||
type=${type}
|
||||
name="uidField"
|
||||
@@ -308,57 +260,56 @@ export class IdentificationStage extends BaseStage<
|
||||
autocomplete=${autocomplete}
|
||||
spellcheck="false"
|
||||
class="pf-c-form-control"
|
||||
value=${initialUserIdentification ?? ""}
|
||||
value=${username ?? ""}
|
||||
required
|
||||
/>`;
|
||||
}
|
||||
|
||||
protected renderPasswordFields(challenge: IdentificationChallenge) {
|
||||
const { allowShowPassword } = challenge;
|
||||
return html`<ak-flow-input-password
|
||||
.inputRef=${this.passwordFieldRef}
|
||||
label=${msg("Password")}
|
||||
input-id="ak-stage-identification-password"
|
||||
class="pf-c-form__group"
|
||||
.errors=${challenge.responseErrors?.password}
|
||||
?allow-show-password=${allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill.password ?? ""}
|
||||
></ak-flow-input-password> `;
|
||||
return html`
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
input-id="ak-stage-identification-password"
|
||||
class="pf-c-form__group"
|
||||
.errors=${challenge.responseErrors?.password}
|
||||
?allow-show-password=${allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill.password ?? ""}
|
||||
></ak-flow-input-password>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderInput(challenge: IdentificationChallenge) {
|
||||
const { flowDesignation, passwordFields, passwordlessUrl, primaryAction, userFields } =
|
||||
challenge;
|
||||
const {
|
||||
flowDesignation,
|
||||
passwordFields,
|
||||
passwordlessUrl,
|
||||
pendingUserIdentifier,
|
||||
primaryAction,
|
||||
userFields,
|
||||
} = challenge;
|
||||
|
||||
const fields = (userFields || []).sort();
|
||||
if (fields.length === 0) {
|
||||
return html`<p>${msg("Select one of the options below to continue.")}</p>`;
|
||||
}
|
||||
|
||||
const {
|
||||
inputID,
|
||||
defaultUserIdentification: initialUserIdentification,
|
||||
rememberMeController,
|
||||
} = this;
|
||||
const { inputID, rememberMe } = this;
|
||||
|
||||
const offerRecovery = flowDesignation === FlowDesignationEnum.Recovery;
|
||||
const type = fields.length === 1 && fields[0] === UserFieldsEnum.Email ? "email" : "text";
|
||||
const label = OR_LIST_FORMATTERS.format(fields.map((f) => UI_FIELDS[f]));
|
||||
const username = rememberMe.username ?? pendingUserIdentifier;
|
||||
|
||||
// When webauthn is enabled, add "webauthn" to autocomplete to enable passkey autofill
|
||||
const autocomplete: AutoFill = this.#webauthn.live ? "username webauthn" : "username";
|
||||
|
||||
console.debug(
|
||||
"Rendering identification stage with fields:",
|
||||
fields,
|
||||
initialUserIdentification,
|
||||
);
|
||||
// prettier-ignore
|
||||
return html`${offerRecovery ? this.renderRecoveryMessage() : nothing}
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: inputID }, label)}
|
||||
${this.renderUidField(inputID, type, label, initialUserIdentification, autocomplete)}
|
||||
${rememberMeController?.renderToggleInput() ?? null}
|
||||
${this.renderUidField(inputID, type, label, username, autocomplete)}
|
||||
${rememberMe.render()}
|
||||
${AKFormErrors({ errors: challenge.responseErrors?.uid_field })}
|
||||
</div>
|
||||
${passwordFields ? this.renderPasswordFields(challenge) : nothing}
|
||||
|
||||
@@ -1,35 +1,11 @@
|
||||
import { StorageAccessor } from "#common/storage";
|
||||
import { getCookie } from "#common/utils";
|
||||
|
||||
import { ReactiveElementHost } from "#elements/types";
|
||||
|
||||
import type { IdentificationStage } from "#flow/stages/identification/IdentificationStage";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, ReactiveController } from "lit";
|
||||
import { createRef, Ref } from "lit-html/directives/ref.js";
|
||||
import { css, html, nothing, ReactiveController, ReactiveControllerHost } from "lit";
|
||||
|
||||
export class RememberMeStorage {
|
||||
static readonly user = StorageAccessor.local("authentik-remember-me-user");
|
||||
static readonly session = StorageAccessor.local("authentik-remember-me-session");
|
||||
static reset = () => {
|
||||
this.user.delete();
|
||||
this.session.delete();
|
||||
};
|
||||
}
|
||||
|
||||
function readSessionID() {
|
||||
return (getCookie("authentik_csrf") ?? "").substring(0, 8);
|
||||
}
|
||||
|
||||
export interface RememberMeControllerInit {
|
||||
pendingUserIdentifier: string | null;
|
||||
identificationFieldRef: Ref<HTMLInputElement>;
|
||||
passwordFieldRef: Ref<HTMLInputElement> | null;
|
||||
identificationFieldID: string;
|
||||
}
|
||||
type RememberMeHost = ReactiveControllerHost & IdentificationStage;
|
||||
|
||||
/**
|
||||
* Remember the user's `username` "on this device."
|
||||
@@ -48,7 +24,7 @@ export interface RememberMeControllerInit {
|
||||
* came back to this view after reaching the identity proof phase, indicating they pressed the "not
|
||||
* you?" link, at which point it begins again to record the username as it is typed in.
|
||||
*/
|
||||
export class RememberMeController implements ReactiveController {
|
||||
export class RememberMe implements ReactiveController {
|
||||
static readonly styles = [
|
||||
css`
|
||||
.remember-me-switch {
|
||||
@@ -59,178 +35,121 @@ export class RememberMeController implements ReactiveController {
|
||||
`,
|
||||
];
|
||||
|
||||
//#region Lifecycle
|
||||
public username?: string;
|
||||
|
||||
public readonly identificationFieldRef: Ref<HTMLInputElement>;
|
||||
public readonly passwordFieldRef: Ref<HTMLInputElement> | null;
|
||||
public readonly defaultChecked: boolean;
|
||||
public readonly defaultUserIdentification: string | null;
|
||||
public readonly identificationFieldID: string;
|
||||
#trackRememberMe = () => {
|
||||
if (!this.#usernameField || this.#usernameField.value === undefined) {
|
||||
return;
|
||||
}
|
||||
this.username = this.#usernameField.value;
|
||||
localStorage?.setItem("authentik-remember-me-user", this.username);
|
||||
};
|
||||
|
||||
protected logger = ConsoleLogger.prefix("controller/remember-me");
|
||||
protected autoSubmitAttempts = 0;
|
||||
protected currentSessionID = readSessionID();
|
||||
// When active, save current details and record every keystroke to the username.
|
||||
// When inactive, clear all fields and remove keystroke recorder.
|
||||
#toggleRememberMe = () => {
|
||||
if (!this.#rememberMeToggle || !this.#rememberMeToggle.checked) {
|
||||
localStorage?.removeItem("authentik-remember-me-user");
|
||||
localStorage?.removeItem("authentik-remember-me-session");
|
||||
this.username = undefined;
|
||||
this.#usernameField?.removeEventListener("keyup", this.#trackRememberMe);
|
||||
return;
|
||||
}
|
||||
if (!this.#usernameField) {
|
||||
return;
|
||||
}
|
||||
localStorage?.setItem("authentik-remember-me-user", this.#usernameField.value);
|
||||
localStorage?.setItem("authentik-remember-me-session", this.#localSession);
|
||||
this.#usernameField.addEventListener("keyup", this.#trackRememberMe);
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected host: ReactiveElementHost<IdentificationStage>,
|
||||
{
|
||||
identificationFieldRef,
|
||||
passwordFieldRef,
|
||||
identificationFieldID,
|
||||
}: RememberMeControllerInit,
|
||||
) {
|
||||
this.identificationFieldRef = identificationFieldRef;
|
||||
this.passwordFieldRef = passwordFieldRef || null;
|
||||
this.identificationFieldID = identificationFieldID;
|
||||
constructor(private host: RememberMeHost) {}
|
||||
|
||||
const persistedSessionID = RememberMeStorage.session.read();
|
||||
// Record a stable token that we can use between requests to track if we've
|
||||
// been here before. If we can't, clear out the username.
|
||||
public hostConnected() {
|
||||
try {
|
||||
const sessionId = localStorage.getItem("authentik-remember-me-session");
|
||||
if (!!this.#localSession && sessionId === this.#localSession) {
|
||||
this.username = undefined;
|
||||
localStorage?.removeItem("authentik-remember-me-user");
|
||||
}
|
||||
localStorage?.setItem("authentik-remember-me-session", this.#localSession);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (_e: any) {
|
||||
this.username = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (persistedSessionID && persistedSessionID !== this.currentSessionID) {
|
||||
this.logger.debug("Session ID mismatch, clearing remembered username");
|
||||
RememberMeStorage.user.delete();
|
||||
get #localSession() {
|
||||
return (getCookie("authentik_csrf") ?? "").substring(0, 8);
|
||||
}
|
||||
|
||||
get #usernameField() {
|
||||
return this.host.renderRoot.querySelector(
|
||||
'input[name="uidField"]',
|
||||
) as HTMLInputElement | null;
|
||||
}
|
||||
|
||||
get #rememberMeToggle() {
|
||||
return this.host.renderRoot.querySelector(
|
||||
"#authentik-remember-me",
|
||||
) as HTMLInputElement | null;
|
||||
}
|
||||
|
||||
get #submitButton() {
|
||||
return this.host.renderRoot.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
}
|
||||
|
||||
get #isEnabled() {
|
||||
return this.host.challenge?.enableRememberMe && typeof localStorage !== "undefined";
|
||||
}
|
||||
|
||||
get #canAutoSubmit() {
|
||||
return (
|
||||
!!this.host.challenge &&
|
||||
!!this.username &&
|
||||
!!this.#usernameField?.value &&
|
||||
!this.host.challenge.passwordFields &&
|
||||
!this.host.challenge.passwordlessUrl
|
||||
);
|
||||
}
|
||||
|
||||
// Before the page is updated, try to extract the username from localstorage.
|
||||
public hostUpdate() {
|
||||
if (!this.#isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const persistedUserIdentifier = RememberMeStorage.user.read();
|
||||
|
||||
this.defaultUserIdentification =
|
||||
persistedUserIdentifier || this.host.challenge?.pendingUserIdentifier || null;
|
||||
|
||||
this.defaultChecked = !!persistedUserIdentifier;
|
||||
try {
|
||||
this.username = localStorage.getItem("authentik-remember-me-user") || undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (_e: any) {
|
||||
this.username = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// After the page is updated, if everything is ready to go, do the autosubmit.
|
||||
public hostUpdated() {
|
||||
if (this.canAutoSubmit() && this.autoSubmitAttempts === 0) {
|
||||
this.autoSubmitAttempts++;
|
||||
this.host.submitForm?.();
|
||||
if (this.#isEnabled && this.#canAutoSubmit) {
|
||||
this.#submitButton?.click();
|
||||
}
|
||||
}
|
||||
|
||||
//#region Event Listeners
|
||||
|
||||
#writeFrameID = -1;
|
||||
|
||||
public inputListener = (event: InputEvent) => {
|
||||
cancelAnimationFrame(this.#writeFrameID);
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
|
||||
this.#writeFrameID = requestAnimationFrame(() => {
|
||||
RememberMeStorage.user.write(value);
|
||||
});
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public API
|
||||
|
||||
/**
|
||||
* Toggle the "remember me" feature on or off.
|
||||
*
|
||||
* When toggled on, the current username is saved to localStorage and will be automatically
|
||||
* submitted on future visits. Additionally, every keystroke in the username field will update
|
||||
* the stored username.
|
||||
*
|
||||
* When toggled off, any stored username is cleared from localStorage, and the keystroke listener
|
||||
* is removed to stop updating the stored username.
|
||||
*/
|
||||
public toggleChangeListener = (event: Event) => {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
const { usernameField, passwordField } = this;
|
||||
|
||||
if (!checkbox.checked) {
|
||||
this.logger.debug("Disabling remember me");
|
||||
|
||||
RememberMeStorage.reset();
|
||||
|
||||
if (usernameField) {
|
||||
usernameField.removeEventListener("input", this.inputListener);
|
||||
usernameField.focus();
|
||||
usernameField.select();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!usernameField) {
|
||||
this.logger.warn("Cannot enable remember me: no username field found");
|
||||
return;
|
||||
}
|
||||
|
||||
const focusTarget = passwordField && usernameField?.value ? passwordField : usernameField;
|
||||
|
||||
if (focusTarget) {
|
||||
focusTarget.focus();
|
||||
focusTarget.select();
|
||||
}
|
||||
|
||||
this.logger.debug("Enabling remember me for user");
|
||||
|
||||
RememberMeStorage.user.write(usernameField.value);
|
||||
RememberMeStorage.session.write(this.currentSessionID);
|
||||
|
||||
usernameField.addEventListener("input", this.inputListener, {
|
||||
passive: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the "remember me" feature can be automatically submitted, which requires:
|
||||
*
|
||||
* - An active challenge.
|
||||
* - A stored username from a previous session.
|
||||
* - The identifier input field to be present in the DOM.
|
||||
* - No password fields or passwordless URL, indicating we can skip directly to the next step.
|
||||
*/
|
||||
public canAutoSubmit(): boolean {
|
||||
const { challenge } = this.host;
|
||||
|
||||
if (!challenge) return false;
|
||||
if (!challenge.enableRememberMe) return false;
|
||||
|
||||
if (challenge.passwordFields) return false;
|
||||
if (challenge.passwordlessUrl) return false;
|
||||
|
||||
if (!this.defaultChecked) return false;
|
||||
return !!this.usernameField?.value;
|
||||
public render() {
|
||||
return this.#isEnabled
|
||||
? html` <label class="pf-c-switch remember-me-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
id="authentik-remember-me"
|
||||
@click=${this.#toggleRememberMe}
|
||||
type="checkbox"
|
||||
?checked=${!!this.username}
|
||||
/>
|
||||
<span class="pf-c-form__label">${msg("Remember me on this device")}</span>
|
||||
</label>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
protected readonly checkboxRef = createRef<HTMLInputElement>();
|
||||
|
||||
protected get usernameField() {
|
||||
return this.identificationFieldRef.value || null;
|
||||
}
|
||||
|
||||
protected get passwordField() {
|
||||
return this.passwordFieldRef?.value || null;
|
||||
}
|
||||
|
||||
protected get checkboxToggle() {
|
||||
return this.checkboxRef.value || null;
|
||||
}
|
||||
public renderToggleInput = () => {
|
||||
return html`<label
|
||||
class="pf-c-switch remember-me-switch"
|
||||
for="authentik-remember-me"
|
||||
aria-description=${msg(
|
||||
"When enabled, your username will be remembered on this device for future logins.",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
id="authentik-remember-me"
|
||||
@change=${this.toggleChangeListener}
|
||||
?checked=${this.defaultChecked}
|
||||
/>
|
||||
<span class="pf-c-form__label">${msg("Remember me on this device")}</span>
|
||||
</label>`;
|
||||
};
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export default RememberMeController;
|
||||
export default RememberMe;
|
||||
|
||||
@@ -179,7 +179,7 @@ test.describe("Groups", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Edit group from view page", async ({ form, pointer, page }, testInfo) => {
|
||||
test("Edit group from view page", async ({ navigator, form, pointer, page }, testInfo) => {
|
||||
const groupName = groupNames.get(testInfo.testId)!;
|
||||
|
||||
const { fill, search } = form;
|
||||
|
||||
@@ -17,7 +17,11 @@ test.describe("Provider Wizard", () => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "New Provider Wizard" });
|
||||
|
||||
await test.step("Authenticate", async () => session.login());
|
||||
await test.step("Authenticate", async () => {
|
||||
await session.login({
|
||||
to: "/if/admin/#/core/providers",
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Navigate to provider wizard", async () => {
|
||||
await expect(dialog, "Dialog is initially closed").toBeHidden();
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import { FormFixture } from "#e2e/fixtures/FormFixture";
|
||||
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
|
||||
import { GOOD_USERNAME, SessionFixture } from "#e2e/fixtures/SessionFixture";
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
const REMEMBER_ME_USER_KEY = "authentik-remember-me-user";
|
||||
const REMEMBER_ME_SESSION_KEY = "authentik-remember-me-session";
|
||||
|
||||
const IDENTIFICATION_STAGE_NAME = "default-authentication-identification";
|
||||
|
||||
const readStoredUserIdentifier = (page: Page) =>
|
||||
page.evaluate((k) => localStorage.getItem(k), REMEMBER_ME_USER_KEY);
|
||||
|
||||
test.describe("Session Lifecycle", () => {
|
||||
test.beforeAll(
|
||||
'Ensure "Enable Remember me on this device" is on for the default identification stage',
|
||||
async ({ browser }, { title: testName }) => {
|
||||
if (Date.now()) return;
|
||||
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const navigator = new NavigatorFixture(page, testName);
|
||||
const form = new FormFixture(page, testName);
|
||||
const session = new SessionFixture({ page, testName, navigator });
|
||||
|
||||
await test.step("Authenticate", async () =>
|
||||
session.login({
|
||||
to: "/if/admin/#/flow/stages",
|
||||
page,
|
||||
}));
|
||||
|
||||
const $stage = await test.step("Find stage via search", () =>
|
||||
form.search(IDENTIFICATION_STAGE_NAME, page));
|
||||
|
||||
await $stage.getByRole("button", { name: "Edit Stage" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Edit Identification Stage" });
|
||||
await expect(dialog, "Edit modal opens after clicking edit").toBeVisible();
|
||||
|
||||
await form.setInputCheck(`Enable "Remember me on this device"`, true, dialog);
|
||||
await dialog.getByRole("button", { name: "Save Changes" }).click();
|
||||
await expect(dialog, "Edit modal closes after save").toBeHidden();
|
||||
|
||||
await context.close();
|
||||
},
|
||||
);
|
||||
|
||||
test.beforeEach(async ({ session, page }) => {
|
||||
await session.toLoginPage();
|
||||
|
||||
await page.evaluate(
|
||||
([userKey, sessionKey]) => {
|
||||
localStorage.removeItem(userKey);
|
||||
localStorage.removeItem(sessionKey);
|
||||
},
|
||||
[REMEMBER_ME_USER_KEY, REMEMBER_ME_SESSION_KEY],
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await session.$identificationStage.waitFor({ state: "visible" });
|
||||
});
|
||||
|
||||
test("Remember me persists username", async ({ navigator, session, page }) => {
|
||||
await test.step("Verify identification stage", async () => {
|
||||
await expect(
|
||||
session.$rememberMeCheckbox,
|
||||
"Remember me checkbox is visible",
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
session.$rememberMeCheckbox,
|
||||
"Remember me checkbox is not checked by default",
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
await test.step("Identify with remember-me enabled", async () => {
|
||||
await session.login(
|
||||
{
|
||||
rememberMe: true,
|
||||
to: "if/user/#/library",
|
||||
},
|
||||
page,
|
||||
);
|
||||
|
||||
const storedUserIdentifier = await readStoredUserIdentifier(page);
|
||||
|
||||
expect(
|
||||
storedUserIdentifier,
|
||||
"username persists to localStorage when remember-me is checked",
|
||||
).toBe(GOOD_USERNAME);
|
||||
});
|
||||
|
||||
await test.step("Sign out and verify username is remembered", async () => {
|
||||
const signOutLink = page.getByRole("link", { name: "Sign out" });
|
||||
|
||||
await expect(signOutLink, "Sign out link is visible").toBeVisible();
|
||||
|
||||
await signOutLink.click();
|
||||
|
||||
await navigator.waitForPathname("/if/flow/default-authentication-flow/?next=%2F");
|
||||
|
||||
const notYouLink = page.getByRole("link", { name: "Not you?" });
|
||||
|
||||
await expect(notYouLink, "Not you? link is visible after sign out").toBeVisible();
|
||||
|
||||
await notYouLink.click();
|
||||
|
||||
await expect(
|
||||
session.$identificationStage,
|
||||
"Identification stage is visible after clicking not you link",
|
||||
).toBeVisible();
|
||||
|
||||
const storedUserIdentifier = await readStoredUserIdentifier(page);
|
||||
|
||||
expect(storedUserIdentifier, "Removed after clicking not you link").toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,11 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"experimentalDecorators": true,
|
||||
// See https://lit.dev/docs/components/properties/
|
||||
|
||||
// `useDefineForClassFields` is required for Lit decorator compatibility.
|
||||
// See the Lit documentation
|
||||
// [Typescript class fields for reactive properties](https://lit.dev/docs/components/properties/#:~:text=Set%20the%20useDefineForClassFields%20compiler%20option%20to%20false)
|
||||
// for details.
|
||||
"useDefineForClassFields": false,
|
||||
"target": "esnext",
|
||||
"module": "preserve",
|
||||
|
||||
@@ -5270,7 +5270,7 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<target>Aktivovat</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Aktualizovat heslo uživatele <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11007,22 +11007,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5295,7 +5295,7 @@ Hier können nur Policies verwendet werden, da der Zugriff geprüft wird, bevor
|
||||
<target>Aktivieren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> - Passwort ändern.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11040,22 +11040,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4089,7 +4089,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Activate</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -9010,22 +9010,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5235,7 +5235,7 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
|
||||
<target>Activar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Actualizar la contraseña de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10965,22 +10965,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5398,7 +5398,7 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
|
||||
<target>Aktivoi</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Päivitä käyttäjän <x id="0" equiv-text="${item.name || item.username}"/> salasana</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11206,22 +11206,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5388,7 +5388,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Activer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Mettre à jour le mot de passe de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11195,22 +11195,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5194,7 +5194,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Attivare</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Aggiorna <x id="0" equiv-text="${item.name || item.username}"/> password</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10914,22 +10914,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5395,7 +5395,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>アクティブ化</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> のパスワードを更新</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11195,22 +11195,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4988,7 +4988,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>활성화</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> 비밀번호 업데이트 </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10562,22 +10562,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4804,7 +4804,7 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
|
||||
<target>Activeren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10246,22 +10246,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5004,7 +5004,7 @@ Można tu używać tylko zasad, ponieważ dostęp jest sprawdzany przed uwierzyt
|
||||
<target>Aktywuj</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10588,22 +10588,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5394,7 +5394,7 @@ Você só pode usar políticas aqui, pois o acesso é verificado antes de o usu
|
||||
<target>Ativar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>Atualizar a senha de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11188,22 +11188,6 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5052,7 +5052,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Активировать</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10674,22 +10674,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5051,7 +5051,7 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
|
||||
<target>Etkinleştir</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10663,22 +10663,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5461,7 +5461,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>激活</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<target>更新 <x id="0" equiv-text="${user.name || user.username}"/> 的密码</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11467,22 +11467,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4839,7 +4839,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>啟用</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10299,22 +10299,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4,23 +4,17 @@ ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
# TODO: Use setup-corepack.mjs
|
||||
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/scripts/node/,src=./scripts/node/ \
|
||||
--mount=type=bind,target=/work/packages/logger-js/,src=./packages/logger-js/ \
|
||||
--mount=type=bind,target=/work/packages/tsconfig/,src=./packages/tsconfig/ \
|
||||
--mount=type=bind,target=/work/packages/eslint-config/,src=./packages/eslint-config/ \
|
||||
--mount=type=bind,target=/work/packages/prettier-config/,src=./packages/prettier-config/ \
|
||||
--mount=type=bind,target=/work/website/package.json,src=./website/package.json \
|
||||
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
|
||||
node ./scripts/node/setup-corepack.mjs --force && \
|
||||
corepack npm ci \
|
||||
node ./scripts/node/lint-runtime.mjs ./website
|
||||
npm install --force -g corepack@latest && \
|
||||
corepack install -g npm@11.11.0+sha512.f36811c4aae1fde639527368ae44c571d050006a608d67a191f195a801a52637a312d259186254aa3a3799b05335b7390539cf28656d18f0591a1125ba35f973 && \
|
||||
corepack enable
|
||||
|
||||
WORKDIR /work/website
|
||||
|
||||
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/scripts/node/,src=./scripts/node/ \
|
||||
--mount=type=bind,target=/work/packages/logger-js/,src=./packages/logger-js/ \
|
||||
--mount=type=bind,target=/work/packages/tsconfig/,src=./packages/tsconfig/ \
|
||||
--mount=type=bind,target=/work/packages/eslint-config/,src=./packages/eslint-config/ \
|
||||
--mount=type=bind,target=/work/packages/prettier-config/,src=./packages/prettier-config/ \
|
||||
@@ -32,9 +26,7 @@ RUN --mount=type=bind,target=/work/package.json,src=./package.json \
|
||||
--mount=type=bind,target=/work/website/integrations/package.json,src=./website/integrations/package.json \
|
||||
--mount=type=bind,target=/work/website/docs/package.json,src=./website/docs/package.json \
|
||||
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
|
||||
corepack npm ci --workspaces --include-workspace-root --prefix ./website
|
||||
|
||||
WORKDIR /work/website
|
||||
corepack npm ci --workspaces --include-workspace-root
|
||||
|
||||
COPY ./website /work/website/
|
||||
COPY ./blueprints /work/blueprints/
|
||||
@@ -42,7 +34,7 @@ COPY ./schema.yml /work/
|
||||
COPY ./lifecycle/container/compose.yml /work/lifecycle/container/
|
||||
COPY ./SECURITY.md /work/
|
||||
|
||||
RUN corepack npm run build -w docs
|
||||
RUN corepack npm run build
|
||||
|
||||
FROM docker.io/library/nginx:1.29-trixie@sha256:6e23479198b998e5e25921dff8455837c7636a67111a04a635cf1bb363d199dc
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Managed Users
|
||||
sidebar_label: GitHub Enterprise EMU
|
||||
support_level: community
|
||||
---
|
||||
|
||||
import TabItem from "@theme/TabItem";
|
||||
import Tabs from "@theme/Tabs";
|
||||
|
||||
## What is GitHub Enterprise Managed Users
|
||||
|
||||
> With Enterprise Managed Users, you manage the lifecycle and authentication of your users on GitHub from an external identity management system, or IdP.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/understanding-iam-for-enterprises/about-enterprise-managed-users
|
||||
|
||||
This guide configures authentik as the SAML identity provider and SCIM provider for GitHub Enterprise Cloud with Enterprise Managed Users (EMU). It applies to EMU enterprises hosted on GitHub.com and EMU enterprises with data residency on GHE.com.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub.com EMU enterprise, where `foo` is the name of your enterprise.
|
||||
- `foo.ghe.com` is your GHE.com EMU enterprise, where `foo` is the name of your enterprise.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub enterprise administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
SCIM must be configured for this integration. GitHub matches the SAML identity to the SCIM identity by comparing the SAML `NameID` value with the SCIM `userName` value. The mappings below use the `github_emu_username` user attribute when it exists, and fall back to the authentik username.
|
||||
|
||||
Use the values for your EMU deployment when configuring authentik:
|
||||
|
||||
<Tabs
|
||||
groupId="github-emu-deployment"
|
||||
defaultValue="github"
|
||||
values={[
|
||||
{label: 'GitHub.com', value: 'github'},
|
||||
{label: 'GHE.com', value: 'ghec'},
|
||||
]}>
|
||||
<TabItem value="github">
|
||||
|
||||
| Setting | Value |
|
||||
| ------------ | ------------------------------------------------- |
|
||||
| **ACS URL** | `https://github.com/enterprises/foo/saml/consume` |
|
||||
| **Audience** | `https://github.com/enterprises/foo` |
|
||||
| **Issuer** | `https://github.com/enterprises/foo` |
|
||||
| **SCIM URL** | `https://api.github.com/scim/v2/enterprises/foo` |
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ghec">
|
||||
|
||||
| Setting | Value |
|
||||
| ------------ | -------------------------------------------------- |
|
||||
| **ACS URL** | `https://foo.ghe.com/enterprises/foo/saml/consume` |
|
||||
| **Audience** | `https://foo.ghe.com/enterprises/foo` |
|
||||
| **Issuer** | `https://foo.ghe.com/enterprises/foo` |
|
||||
| **SCIM URL** | `https://api.foo.ghe.com/scim/v2/enterprises/foo` |
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise EMU with authentik, you need to create property mappings, an application/provider pair, application entitlements, and a SCIM provider.
|
||||
|
||||
### Create property mappings in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
3. Create the following **SAML Provider Property Mapping**s:
|
||||
- **Name**: `GitHub EMU username`
|
||||
- **SAML Attribute Name**: `http://schemas.goauthentik.io/2021/02/saml/username`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return request.user.attributes.get("github_emu_username", request.user.username)
|
||||
```
|
||||
|
||||
- **Name**: `GitHub EMU full name`
|
||||
- **SAML Attribute Name**: `full_name`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return request.user.name
|
||||
```
|
||||
|
||||
- **Name**: `GitHub EMU emails`
|
||||
- **SAML Attribute Name**: `emails`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
if request.user.email:
|
||||
yield request.user.email
|
||||
```
|
||||
|
||||
4. Create a **SCIM Provider Mapping** with the following settings:
|
||||
- **Name**: `GitHub EMU user`
|
||||
- **Expression**:
|
||||
|
||||
The supported `roles` values are documented in [GitHub Enterprise Cloud's SCIM API documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user).
|
||||
|
||||
```python
|
||||
username = request.user.attributes.get("github_emu_username", request.user.username)
|
||||
formatted = request.user.name or username
|
||||
given_name = formatted
|
||||
family_name = " "
|
||||
if " " in formatted:
|
||||
given_name, _, family_name = formatted.partition(" ")
|
||||
|
||||
emails = []
|
||||
if request.user.email:
|
||||
emails.append(
|
||||
{
|
||||
"value": request.user.email,
|
||||
"type": "work",
|
||||
"primary": True,
|
||||
}
|
||||
)
|
||||
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({"value": "enterprise_owner", "primary": True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({"value": "user", "primary": True})
|
||||
|
||||
return {
|
||||
"userName": username,
|
||||
"externalId": str(request.user.uid),
|
||||
"name": {
|
||||
"formatted": formatted,
|
||||
"givenName": given_name,
|
||||
"familyName": family_name,
|
||||
},
|
||||
"displayName": formatted,
|
||||
"active": request.user.is_active,
|
||||
"emails": emails,
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to the ACS URL for your EMU deployment.
|
||||
- Set **Audience** to the audience value for your EMU deployment.
|
||||
- Set **Issuer** to the issuer value for your EMU deployment.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**:
|
||||
- Add the `GitHub EMU full name` and `GitHub EMU emails` property mappings.
|
||||
- Set **NameID Property Mapping** to `GitHub EMU username`.
|
||||
- Set **Default NameID Policy** to `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`.
|
||||
- Select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- Enable **Sign assertion** and **Sign response**.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application are synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create application entitlements
|
||||
|
||||
1. In the authentik Admin interface, open the GitHub EMU application that you created.
|
||||
2. Click the **Application entitlements** tab.
|
||||
3. Create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
4. Open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## GitHub configuration
|
||||
|
||||
When GitHub provisions your managed enterprise, GitHub sends an email inviting you to reset the password for the setup user. The setup user has the username `foo_admin`, cannot be linked with SSO, and is the emergency account that can bypass SSO requirements.
|
||||
|
||||
### Create the SCIM token
|
||||
|
||||
1. Log in as the setup user.
|
||||
2. Navigate to the personal access tokens page:
|
||||
- GitHub.com: `https://github.com/settings/tokens`
|
||||
- GHE.com: `https://foo.ghe.com/settings/tokens`
|
||||
3. Generate a new classic personal access token with the `scim:enterprise` scope.
|
||||
4. Copy the token. This value is used in the authentik SCIM provider.
|
||||
|
||||
### Configure SAML in GitHub
|
||||
|
||||
1. Log in as the setup user.
|
||||
2. Navigate to your enterprise.
|
||||
3. Click **Identity provider**.
|
||||
4. Under **Identity Provider**, click **Single sign-on configuration**.
|
||||
5. Under **Open SCIM Configuration**, select **Enable open SCIM configuration**.
|
||||
6. Under **SAML single sign-on**, select **Add SAML configuration**.
|
||||
7. Configure the following settings:
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Public certificate**: paste the full signing certificate that you downloaded from authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
8. Click **Test SAML configuration**.
|
||||
9. After the test succeeds, click **Save SAML settings**.
|
||||
10. Save the SAML recovery codes that GitHub provides.
|
||||
|
||||

|
||||
|
||||
### Create a SCIM provider in authentik
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** > **Providers** and click **Create**.
|
||||
2. Select **SCIM Provider** as the provider type and click **Next**.
|
||||
3. Configure the following settings:
|
||||
- **Name**: provide a descriptive name.
|
||||
- **URL**: enter the SCIM URL for your EMU deployment.
|
||||
- **Token**: paste the GitHub personal access token that you created earlier.
|
||||
- **User Property Mappings**: remove `authentik default SCIM Mapping: User`, then add the `GitHub EMU user` mapping that you created earlier.
|
||||
- **Group Property Mappings**: keep `authentik default SCIM Mapping: Group` selected.
|
||||
4. Click **Finish**.
|
||||
5. Navigate to **Applications** > **Applications** and open the GitHub EMU application.
|
||||
6. Add the SCIM provider to **Backchannel Providers**.
|
||||
7. Click **Update**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise EMU, assign a test user to the `GitHub Users` entitlement and ensure that the user can view the application in authentik.
|
||||
|
||||
Open the SCIM provider and click **Run sync again**. After the sync completes, confirm that the user is provisioned in GitHub. Then, log in to GitHub as the test user and confirm that GitHub redirects the user to authentik for SAML authentication.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Cloud: configuring SAML single sign-on for Enterprise Managed Users](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/configuring-authentication-for-enterprise-managed-users/configuring-saml-single-sign-on-for-enterprise-managed-users)
|
||||
- [GitHub Enterprise Cloud: configuring SCIM provisioning for Enterprise Managed Users](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/provisioning-user-accounts-with-scim/configuring-scim-provisioning-for-users)
|
||||
- [GitHub Enterprise Cloud: REST API endpoints for SCIM](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/scim)
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud
|
||||
sidebar_label: GitHub Enterprise Cloud
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud
|
||||
|
||||
> GitHub Enterprise Cloud is a plan for large businesses or teams who collaborate on GitHub.com.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/get-started/learning-about-github/githubs-plans
|
||||
|
||||
This guide configures SAML SSO for a GitHub Enterprise Cloud organization.
|
||||
|
||||
:::info
|
||||
For GitHub Enterprise Cloud with Enterprise Managed Users, see the [GitHub Enterprise EMU](../ghec-emu/) integration guide.
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/orgs/foo` is your GitHub organization, where `foo` is the name of your organization.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to `https://github.com/orgs/foo/saml/consume`.
|
||||
- Set **Audience** to `https://github.com/orgs/foo`.
|
||||
- Set **Issuer** to `https://github.com/orgs/foo`.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub configuration
|
||||
|
||||
1. Log in to GitHub as an organization owner.
|
||||
2. Navigate to your organization at `https://github.com/foo`.
|
||||
3. Click **Settings**.
|
||||
4. In the left sidebar, under **Security**, click **Authentication security**.
|
||||
5. Under **SAML single sign-on**, select **Enable SAML authentication**.
|
||||
6. Configure the following settings:
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Public certificate**: paste the full signing certificate that you downloaded from authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
7. Click **Test SAML configuration**.
|
||||
8. After the test succeeds, click **Save**.
|
||||
|
||||

|
||||
|
||||
This enables SAML as an authentication option. To require SAML for all organization members, visit `https://github.com/orgs/foo/sso`, sign in with SAML, then return to **Authentication security** and select **Require SAML SSO authentication for all members of the foo organization**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise Cloud, log out of GitHub and then access a resource in the organization. GitHub should prompt you to authenticate with SAML through authentik.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Cloud: managing SAML single sign-on for your organization](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-saml-single-sign-on-for-your-organization)
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Server
|
||||
sidebar_label: GitHub Enterprise Server
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Server
|
||||
|
||||
> GitHub Enterprise Server is a self-hosted platform for software development within your enterprise.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.company` is the FQDN of your GitHub Enterprise Server installation.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Server users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub Enterprise Server administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Server with authentik, you need to create an application/provider pair in authentik. If you want to use SCIM provisioning, you also need to create application entitlements and a SCIM property mapping.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to `https://github.company/saml/consume`.
|
||||
- Set **Audience** to `https://github.company`.
|
||||
- Set **Issuer** to `https://github.company`.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**:
|
||||
- Select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- Set **NameID Property Mapping** to `authentik default SAML Mapping: Username`.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application are synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create application entitlements
|
||||
|
||||
1. In the authentik Admin interface, open the GitHub Enterprise Server application that you created.
|
||||
2. Click the **Application entitlements** tab.
|
||||
3. Create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
4. Open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
### Create a SCIM property mapping
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
2. Select **SCIM Provider Mapping** and click **Next**.
|
||||
3. Create a mapping for GitHub roles:
|
||||
- **Name**: `GitHub roles`
|
||||
- **Expression**:
|
||||
|
||||
The supported `roles` values are documented in [GitHub Enterprise Server's SCIM API documentation](https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user).
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({"value": "enterprise_owner", "primary": True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({"value": "user", "primary": True})
|
||||
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
4. Click **Finish**.
|
||||
|
||||
## GitHub Enterprise Server configuration
|
||||
|
||||
### Create the SCIM token
|
||||
|
||||
1. Log in to GitHub Enterprise Server with the administrator account that you use for SCIM provisioning.
|
||||
2. Navigate to `https://github.company/settings/tokens`.
|
||||
3. Generate a new classic personal access token with the `scim:enterprise` scope.
|
||||
4. Copy the token. This value is used in the authentik SCIM provider.
|
||||
|
||||
### Configure SAML
|
||||
|
||||
1. Navigate to the GitHub Enterprise Server Management Console at `https://github.company:8443`.
|
||||
2. Sign in as an administrator.
|
||||
3. Go to **Authentication**.
|
||||
4. Configure the following settings:
|
||||
- Select **SAML**.
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
- **Validation certificate**: upload the signing certificate that you downloaded from authentik.
|
||||
- If you plan to use SCIM, select **Allow creation of accounts with built-in authentication** and **Disable administrator demotion/promotion**.
|
||||
- In the **User attributes** section, do not configure a different username attribute unless it returns the same value as the SCIM `userName` attribute.
|
||||
5. Click **Save settings** and wait for the changes to apply.
|
||||
|
||||

|
||||
|
||||
### Enable SCIM
|
||||
|
||||
1. Log in to GitHub Enterprise Server with an administrator account.
|
||||
2. Open **Enterprise settings**.
|
||||
3. In the left sidebar, click **Settings** > **Authentication security**.
|
||||
4. Select **Enable SCIM configuration**.
|
||||
5. Click **Save**.
|
||||
|
||||
### Create a SCIM provider in authentik
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** > **Providers** and click **Create**.
|
||||
2. Select **SCIM Provider** as the provider type and click **Next**.
|
||||
3. Configure the following settings:
|
||||
- **Name**: provide a descriptive name.
|
||||
- **URL**: `https://github.company/api/v3/scim/v2`
|
||||
- **Token**: paste the GitHub personal access token that you created earlier.
|
||||
- **User Property Mappings**: keep `authentik default SCIM Mapping: User` selected, then add the `GitHub roles` mapping that you created earlier.
|
||||
- **Group Property Mappings**: keep `authentik default SCIM Mapping: Group` selected.
|
||||
4. Click **Finish**.
|
||||
5. Navigate to **Applications** > **Applications** and open the GitHub Enterprise Server application.
|
||||
6. Add the SCIM provider to **Backchannel Providers**.
|
||||
7. Click **Update**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise Server, assign a test user to the `GitHub Users` entitlement and ensure that the user can view the application in authentik.
|
||||
|
||||
Open the SCIM provider and click **Run sync again**. After the sync completes, confirm that the user is provisioned in GitHub Enterprise Server. Then, log in to GitHub Enterprise Server as the test user and confirm that GitHub redirects the user to authentik for SAML authentication.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Server: configuring SAML single sign-on for your enterprise](https://docs.github.com/en/enterprise-server@latest/admin/managing-iam/using-saml-for-enterprise-iam/configuring-saml-single-sign-on-for-your-enterprise)
|
||||
- [GitHub Enterprise Server: REST API endpoints for SCIM](https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/scim)
|
||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud
|
||||
sidebar_label: GitHub Enterprise Cloud
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud
|
||||
|
||||
> GitHub is a complete developer platform to build, scale, and deliver secure software. Businesses use our suite of products to support the entire software development lifecycle, increasing development velocity and improving code quality.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/overview/about-github-for-enterprises
|
||||
|
||||
:::info
|
||||
GitHub Enterprise Cloud EMU (Enterprise Managed Users) are not compatible with authentik. GitHub currently only permits SAML/OIDC for EMU organizations with Okta and/or Microsoft Entra ID (Azure AD).
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub organization, where `foo` is the name of your enterprise.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/enterprises/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Issuer** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Navigate to your enterprise settings by clicking your GitHub user portrait in the top right of GitHub.com, then select `Your enterprises` and click `Settings` for the enterprise you wish to configure.
|
||||
|
||||
In the left-hand navigation, within the `Settings` section, click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Require SAML authentication` checkbox.
|
||||
- In `Sign on URL`, type `https://authentik.company/application/saml/<application_slug>/sso/binding/redirect/`
|
||||
- For `Issuer`, type `https://github.com/enterprises/foo` or the `Audience` you set in authentik
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||

|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save` button below.
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
130
website/integrations/development/github-enterprise-emu/index.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud - Enterprise Managed Users
|
||||
sidebar_label: GitHub Enterprise Cloud EMU
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud - Enterprise Managed Users
|
||||
|
||||
> With Enterprise Managed Users, you manage the lifecycle and authentication of your users on GitHub from an external identity management system, or IdP:
|
||||
>
|
||||
> - Your IdP provisions new user accounts on GitHub, with access to your enterprise.
|
||||
> - Users must authenticate on your IdP to access your enterprise's resources on GitHub.
|
||||
> - You control usernames, profile data, organization membership, and repository access from your IdP.
|
||||
> - If your enterprise uses OIDC SSO, GitHub will validate access to your enterprise and its resources using your IdP's Conditional Access Policy (CAP). See "About support for your IdP's Conditional Access Policy."
|
||||
> - Managed user accounts cannot create public content or collaborate outside your enterprise. See "Abilities and restrictions of managed user accounts."
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/understanding-iam-for-enterprises/about-enterprise-managed-users
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub organization, where `foo` is the name of your enterprise
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Cloud EMU users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub enterprise administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud EMU with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
:::info
|
||||
In order to use GitHub Enterprise Cloud EMU, SCIM must also be set up.
|
||||
:::
|
||||
|
||||
:::info
|
||||
GitHub will create usernames for your EMU users based on the SAML `NameID` property, which must also match SCIM's `_userName_` attribute.
|
||||
:::
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/enterprises/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Issuer** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- Under **NameID Property Mapping**, set **NameID Property Mapping** to be based on the `Email` field.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application will be synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
**Create the user and administrator entitlements**
|
||||
|
||||
In the authentik Admin interface, open the GitHub EMU application that you just created, click the **Application entitlements** tab, and create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
|
||||
After creating the entitlements, open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## GitHub SAML Configuration
|
||||
|
||||
When your EMU is provisioned by GitHub, you will receive an email inviting you to reset the password of your 'setup user'. This user cannot be linked with SSO and is an emergency access account, as it will be the only account that can bypass SSO requirements.
|
||||
|
||||
Before enabling SAML, go to your [Personal access tokens](https://github.com/settings/tokens) on your EMU setup user and Generate a new _personal access token (classic)_. This should have a descriptive note like `SCIM Token`. It is advisable to set this to not expire. For scopes, select only _admin:enterprise_ and click _Generate token_.
|
||||
|
||||
Copy the resulting token to a safe location.
|
||||
|
||||
After you have set a password for this account and generated your SCIM token, navigate to your enterprise settings by clicking your GitHub user portrait in the top right of GitHub.com, select `Your enterprise`, click the `Settings` link, and then click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Require SAML authentication` checkbox.
|
||||
- In `Sign on URL`, input the _SSO URL (Redirect)_ entry from the SAML provider you created.
|
||||
- For `Issuer`, input the `Issuer` you set in authentik.
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||

|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save SAML settings` button below.
|
||||
|
||||
You will now be prompted to save your SAML recovery codes. These will be necessary if you need to disable or change your SAML settings, so keep them safe!
|
||||
|
||||
## SCIM Provider
|
||||
|
||||
Before we create a SCIM provider, we also have to create a new Property Mapping. In authentik, go to _Customization_, then _Property Mappings_. Here, click _Create_, select _SCIM Provider Mapping_. Name the mapping something memorable and paste the following code in the _Expression_ field:
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
# Edit this if statement if you need to add more GitHub roles.
|
||||
# Valid roles include:
|
||||
# user, guest_collaborator, enterprise_owner, billing_manager
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({'value': 'enterprise_owner', 'primary': True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({'value': 'user', 'primary': True})
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
If you renamed either entitlement, make sure that you update the code above to match.
|
||||
|
||||
Create a new SCIM provider with the following parameters:
|
||||
|
||||
- URL: `https://api.github.com/scim/v2/enterprises/foo/` (Replacing `foo` with your Enterprise slug.)
|
||||
- Token: Paste the token provided from GitHub here.
|
||||
- In the _Attribute mapping_ section, de-select the `authentik default SCIM Mapping: User` mapping by selecting it on the right-hand side and clicking the left-facing single chevron.
|
||||
- Select the property mapping you created in the previous step and add it by clicking the right-facing single chevron.
|
||||
- You can leave the _Group Property Mappings_ as is.
|
||||
- Click _Finish_.
|
||||
|
||||
Go back to your GitHub EMU Application created in the first step and add your new SCIM provider in the _Backchannel Providers_ field, then click the _Update_ button.
|
||||
|
||||
You should now be ready to assign users or groups to your _GitHub Users_ and _GitHub Admins_ application entitlements. Use application bindings or policies to limit which users can view the application and are synchronized by SCIM, and use the entitlements to assign the corresponding GitHub SCIM role values. If you do not see your users being provisioned, go to your SCIM provider and click the _Run sync again_ option. A few seconds later, you should see results of the SCIM sync.
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Server
|
||||
sidebar_label: GitHub Enterprise Server
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Server
|
||||
|
||||
> GitHub Enterprise Server is a self-hosted platform for software development within your enterprise. Your team can use GitHub Enterprise Server to build and ship software using Git version control, powerful APIs, productivity and collaboration tools, and integrations. Developers familiar with GitHub.com can onboard and contribute seamlessly using familiar features and workflows.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-server@3.5/admin/overview/about-github-enterprise-server
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `https://github.company` is your GitHub Enterprise Server installation
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Server users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub Enterprise Server administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Server with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
:::info
|
||||
In order to use GitHub Enterprise Server, SCIM must also be set up.
|
||||
:::
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.company/saml/consume`.
|
||||
- Set the **Audience** and **Issuer** to `https://github.company`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application will be synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create the user and administrator entitlements
|
||||
|
||||
In the authentik Admin interface, open the GitHub Enterprise Server application that you just created, click the **Application entitlements** tab, and create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
|
||||
After creating the entitlements, open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## SAML Configuration
|
||||
|
||||
If you plan to use SCIM (available from GHES 3.14.0), create a first administrator user on your instance and go to your personal access tokens at `https://github.company/settings/tokens/new`, click _Generate new token_, and then click _Generate new token (classic)_. Your token should have a descriptive name and, ideally, no expiration date. For permission scopes, you need to select _admin:enterprise_. Click _Generate token_ and store the resulting token in a safe location.
|
||||
|
||||
To enable SAML, navigate to your appliance maintenance settings. These are found at `https://github.company:8443`. Here, sign in with an administrator user and go to the Authentication section.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the _SAML_ option.
|
||||
- In _Sign on URL_, input your _SSO URL (Redirect)_ from authentik.
|
||||
- For _Issuer_, use the _Audience_ you set in authentik.
|
||||
- Verify that the _Signature method_ and _Digest method_ match your SAML provider settings in authentik.
|
||||
- For _Validation certificate_, upload the signing certificate you downloaded after creating the provider.
|
||||
- If you plan to enable SCIM, select _Allow creation of accounts with built-in authentication_ and _Disable administrator demotion/promotion_ options. These are selected so you can use your administrator user as an emergency non-SSO account, as well as create machine users, and to ensure users are not promoted outside your IdP.
|
||||
- In the _User attributes_ section, enter `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` in the _Username_ field to ensure the emails become normalized into usernames in GitHub.
|
||||
- Press Save settings on the left-hand side and wait for the changes to apply.
|
||||
|
||||

|
||||
|
||||
Once the appliance has saved the settings and reloaded the services, you should be able to navigate to your instance URL at `https://github.company` and sign in with SAML.
|
||||
|
||||
## SCIM Configuration
|
||||
|
||||
This section only applies if you completed the steps above to prepare the instance for SCIM enablement.
|
||||
|
||||
After enabling SAML, log into your initial administrator account again. Click the user portrait in the top right, click _Enterprise settings_, click _Settings_ in the left-hand sidebar, and then click _Authentication security_. On this page, check _Enable SCIM configuration_ and press _Save_. After that, you should see a message reading _SCIM Enabled_.
|
||||
|
||||
Before we create a SCIM provider, we have to create a new Property Mapping. In authentik, go to _Customization_, then _Property Mappings_. Here, click _Create_, select _SCIM Provider Mapping_. Name the mapping something memorable and paste the following code in the _Expression_ field:
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
# Edit this if statement if you need to add more GitHub roles.
|
||||
# Valid roles include:
|
||||
# user, guest_collaborator, enterprise_owner, billing_manager
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({'value': 'enterprise_owner', 'primary': True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({'value': 'user', 'primary': True})
|
||||
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
If you renamed either entitlement, make sure that you update the code above to match.
|
||||
|
||||
Create a new SCIM provider with the following parameters:
|
||||
|
||||
- URL: `https://github.company/api/v3/scim/v2`
|
||||
- Token: Paste the token you generated earlier here.
|
||||
- In the _Attribute mapping_ section, de-select the `authentik default SCIM Mapping: User` mapping from the _User Property Mappings_ by selecting it on the right-hand side and clicking the left-facing single chevron.
|
||||
- Select the property mapping you created in the previous step and add it by clicking the right-facing single chevron.
|
||||
- Ensure that `authentik default SCIM Mapping: Group` is the only one active in the _Group Property Mappings_.
|
||||
- Click _Finish_.
|
||||
|
||||
Go back to your GitHub Enterprise Server Application created in the first step and add your new SCIM provider in the _Backchannel Providers_ field, then click the _Update_ button.
|
||||
|
||||
You should now be ready to assign users or groups to your _GitHub Users_ and _GitHub Admins_ application entitlements. Use application bindings or policies to limit which users can view the application and are synchronized by SCIM, and use the entitlements to assign the corresponding GitHub SCIM role values. If you do not see your users being provisioned, go to your SCIM provider and click the _Run sync again_ option. A few seconds later, you should see results of the SCIM sync.
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Integrate with GitHub Organization
|
||||
sidebar_label: GitHub Organization
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is a GitHub Organization
|
||||
|
||||
> Organizations are shared accounts where businesses and open-source projects can collaborate across many projects at once, with sophisticated security and administrative features.
|
||||
>
|
||||
> -- https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/orgs/foo` is your GitHub organization, where `foo` is the name of your GitHub organization.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Organization with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings. Take note of the **slug** as it will be required later.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/orgs/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/orgs/foo`.
|
||||
- Set the **Issuer** to `https://github.com/orgs/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Navigate to your organization settings by going to your organization page at https://github.com/foo, then click Settings.
|
||||
|
||||
In the left-hand navigation, scroll down to the Security section and click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Enable SAML authentication` checkbox.
|
||||
- In `sign-on URL`, type `https://authentik.company/application/saml/<application_slug>/sso/binding/redirect/`
|
||||
- For `Issuer`, type `https://github.com/orgs/foo` or the `Audience` you set in authentik
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save` button below.
|
||||
|
||||

|
||||
|
||||
This enables SAML as an authentication _option_. If you want to _require_ SAML for your organization, visit your SSO url at `https://github.com/orgs/foo/sso` and sign in. Once signed in, you can navigate back to the `Authentication security` page and check `Require SAML SSO authentication for all members of the foo organization.`
|
||||
@@ -31,6 +31,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,4 @@
|
||||
/integrations/* /:splat 301!
|
||||
#endregion
|
||||
|
||||
#region GitHub integration renames
|
||||
/development/github-enterprise-cloud /development/ghec 301!
|
||||
/development/github-enterprise-cloud/ /development/ghec/ 301!
|
||||
|
||||
/development/github-organization /development/ghec 301!
|
||||
/development/github-organization/ /development/ghec/ 301!
|
||||
|
||||
/development/github-enterprise-emu /development/ghec-emu 301!
|
||||
/development/github-enterprise-emu/ /development/ghec-emu/ 301!
|
||||
|
||||
/development/github-enterprise-server /development/ghes 301!
|
||||
/development/github-enterprise-server/ /development/ghes/ 301!
|
||||
#endregion
|
||||
|
||||
/networking/cloudflare-access /security/cloudflare-access 301!
|
||||
|
||||
5
website/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "@goauthentik/docs",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"vendored/*",
|
||||
@@ -202,7 +203,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
},
|
||||
"docusaurus-theme": {
|
||||
@@ -246,7 +247,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.10.1"
|
||||
"npm": ">=11.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/abtesting": {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"build:integrations": "npm run build -w integrations",
|
||||
"check-types": "tsc -b",
|
||||
"docusaurus": "docusaurus",
|
||||
"preinstall": "npm ci --prefix ..",
|
||||
"lint": "eslint --fix .",
|
||||
"lint:lockfile": "echo 'Skipping lockfile linting'",
|
||||
"lint-check": "eslint --max-warnings 0 .",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier-check": "prettier --check .",
|
||||
|
||||