Merge branch 'main' into dev

* main: (52 commits)
  web: provide a test framework (#9681)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#10272)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#10271)
  core: bump drf-jsonschema-serializer from 2.0.0 to 3.0.0 (#10262)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#10268)
  core: bump goauthentik.io/api/v3 from 3.2024042.13 to 3.2024060.1 (#10260)
  core, web: update translations (#10259)
  website/docs: update geoip and asn documentation following field changes (#10265)
  web: provide better feedback on Application Library page about search results (#9386)
  web: disable reading dark mode out of the UI by default (#10256)
  web/flows: remove continue button from AutoSubmit stage (#10253)
  web: bump API Client version (#10252)
  website/docs: update geoip and asn example to use the proper syntax (cherry-pick #10249) (#10250)
  website/docs: update the Welcome page (#10222)
  website/docs: update geoip and asn example to use the proper syntax (#10249)
  security: update supported versions (cherry-pick #10247) (#10248)
  security: update supported versions (#10247)
  website/docs: remove RC disclaimer from 2024.6 release notes (cherry-pick #10245) (#10246)
  website/docs: remove RC disclaimer from 2024.6 release notes (#10245)
  website/docs: update 2024.6 release notes with latest changes (cherry-pick #10228) (#10243)
  ...
This commit is contained in:
Ken Sternberg
2024-06-27 10:21:12 -07:00
74 changed files with 12020 additions and 8057 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.2
current_version = 2024.6.0
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -128,3 +128,21 @@ jobs:
- name: build
working-directory: web/
run: npm run build
test:
needs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test

View File

@ -155,8 +155,8 @@ jobs:
- uses: actions/checkout@v4
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis

View File

@ -14,8 +14,8 @@ jobs:
- uses: actions/checkout@v4
- name: Pre-release test
run: |
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker buildx install
mkdir -p ./gen-ts-api
docker build -t testing:latest .

View File

@ -47,8 +47,8 @@ test-go:
go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis

View File

@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| --------- | --------- |
| 2023.10.x | ✅ |
| 2024.2.x | ✅ |
| Version | Supported |
| -------- | --------- |
| 2024.4.x | ✅ |
| 2024.6.x | ✅ |
## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.4.2"
__version__ = "2024.6.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -44,6 +44,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -13,9 +13,8 @@ from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
@ -24,7 +23,7 @@ class TestTokenAPI(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="testuser")
self.user = create_test_user()
self.admin = create_test_admin_user()
self.client.force_login(self.user)
@ -154,6 +153,24 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)
def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()

View File

@ -4,9 +4,10 @@ from urllib.parse import urlencode
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
@ -77,3 +78,23 @@ class TesOAuth2DeviceInit(OAuthTestCase):
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code}),
)
def test_device_init_denied(self):
"""Test device init"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.application,
order=0,
)
token = DeviceToken.objects.create(
user_code="foo",
provider=self.provider,
)
res = self.client.get(
reverse("authentik_providers_oauth2_root:device-login")
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code})
)
self.assertEqual(res.status_code, 200)
self.assertIn(b"Permission denied", res.content)

View File

@ -11,10 +11,11 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.throttling import AnonRateThrottle
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
LOGGER = get_logger()
@ -37,7 +38,9 @@ class DeviceView(View):
).first()
if not provider:
return HttpResponseBadRequest()
if not get_application(provider):
try:
_ = provider.application
except Application.DoesNotExist:
return HttpResponseBadRequest()
self.provider = provider
self.client_id = client_id

View File

@ -1,8 +1,9 @@
"""Device flow views"""
from typing import Any
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -16,7 +17,8 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
OAuthDeviceCodeFinishStage,
@ -31,60 +33,52 @@ LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec
def get_application(provider: OAuth2Provider) -> Application | None:
"""Get application from provider"""
try:
app = provider.application
if not app:
class CodeValidatorView(PolicyAccessView):
"""Helper to validate frontside token"""
def __init__(self, code: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.code = code
def resolve_provider_application(self):
self.token = DeviceToken.objects.filter(user_code=self.code).first()
if not self.token:
raise Application.DoesNotExist
self.provider = self.token.provider
self.application = self.token.provider.application
def get(self, request: HttpRequest, *args, **kwargs):
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: self.token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
return app
except Application.DoesNotExist:
return None
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
"""Validate user token"""
token = DeviceToken.objects.filter(
user_code=code,
).first()
if not token:
return None
app = get_application(token.provider)
if not app:
return None
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.token.provider.authorization_flow.slug,
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=token.provider.authorization_flow.slug,
)
class DeviceEntryView(View):
class DeviceEntryView(PolicyAccessView):
"""View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
@ -94,7 +88,9 @@ class DeviceEntryView(View):
LOGGER.info("Brand has no device code flow configured", brand=brand)
return HttpResponse(status=404)
if QS_KEY_CODE in request.GET:
validation = validate_code(request.GET[QS_KEY_CODE], request)
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
request
)
if validation:
return validation
LOGGER.info("Got code from query parameter but no matching token found")
@ -131,7 +127,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = validate_code(code, self.stage.request)
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
if not response:
raise ValidationError(_("Invalid code"), "invalid")
return response

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.4.2 Blueprint schema",
"title": "authentik 2024.6.0 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024042.13
goauthentik.io/api/v3 v3.2024060.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024042.13 h1:eklVXXLH0tV+02puhxzWJZ8l6HhxmeVMYp/M6sdaji8=
goauthentik.io/api/v3 v3.2024042.13/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024060.1 h1:X/82DZNlkxztNtJ949mTT9MAIWRKsIg7ROzZHHkjWq4=
goauthentik.io/api/v3 v3.2024060.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.4.2"
const VERSION = "2024.6.0"

View File

@ -16,7 +16,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-05 00:07+0000\n"
"POT-Creation-Date: 2024-06-16 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Anton, 2024\n"
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
@ -394,7 +394,7 @@ msgstr "Домой"
#: authentik/core/templates/login/base_full.html
msgid "Powered by authentik"
msgstr "Поддерживается authentik"
msgstr "Основано на authentik"
#: authentik/core/views/apps.py authentik/providers/oauth2/views/authorize.py
#: authentik/providers/oauth2/views/device_init.py
@ -463,6 +463,22 @@ msgstr "Для доступа к этой функции требуется Ente
msgid "Feature only accessible for internal users."
msgstr "Функция доступна только для внутренних пользователей."
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider User"
msgstr "Пользователь провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Users"
msgstr "Пользователи провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Group"
msgstr "Группа провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Groups"
msgstr "Группы провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
#: authentik/enterprise/providers/microsoft_entra/models.py
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
@ -485,21 +501,17 @@ msgstr "Сопоставление провайдера Google Workspace"
msgid "Google Workspace Provider Mappings"
msgstr "Сопоставления провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider User"
msgstr "Пользователь провайдера Google Workspace"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider User"
msgstr "Пользователь провайдера Microsoft Entra"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Users"
msgstr "Пользователи провайдера Google Workspace"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider Group"
msgstr "Группа провайдера Microsoft Entra"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Group"
msgstr "Группа провайдера Google Workspace"
#: authentik/enterprise/providers/google_workspace/models.py
msgid "Google Workspace Provider Groups"
msgstr "Группы провайдера Google Workspace"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider Groups"
msgstr "Группы провайдера Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider"
@ -517,18 +529,6 @@ msgstr "Сопоставление провайдера Microsoft Entra"
msgid "Microsoft Entra Provider Mappings"
msgstr "Сопоставления провайдера Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider User"
msgstr "Пользователь провайдера Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider Group"
msgstr "Группа провайдера Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider Groups"
msgstr "Группы провайдера Microsoft Entra"
#: authentik/enterprise/providers/rac/models.py
#: authentik/stages/user_login/models.py
msgid ""

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.4.2",
"private": true
"name": "@goauthentik/authentik",
"version": "2024.6.0",
"private": true
}

70
poetry.lock generated
View File

@ -1106,33 +1106,33 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"]
[[package]]
name = "debugpy"
version = "1.8.1"
version = "1.8.2"
description = "An implementation of the Debug Adapter Protocol for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"},
{file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"},
{file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"},
{file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"},
{file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"},
{file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"},
{file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"},
{file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"},
{file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"},
{file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"},
{file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"},
{file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"},
{file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"},
{file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"},
{file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"},
{file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"},
{file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"},
{file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"},
{file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"},
{file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"},
{file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"},
{file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"},
{file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"},
{file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"},
{file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"},
{file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"},
{file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"},
{file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"},
{file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"},
{file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"},
{file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"},
{file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"},
{file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"},
{file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"},
{file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"},
{file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"},
{file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"},
{file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"},
{file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"},
{file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"},
{file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"},
{file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"},
{file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"},
{file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"},
]
[[package]]
@ -1425,17 +1425,17 @@ websockets = ["websocket-client (>=1.3.0)"]
[[package]]
name = "drf-jsonschema-serializer"
version = "2.0.0"
version = "3.0.0"
description = "JSON Schema support for Django REST Framework"
optional = false
python-versions = "*"
python-versions = ">=3.10"
files = [
{file = "drf-jsonschema-serializer-2.0.0.tar.gz", hash = "sha256:bfeea9f2e49f6f14d12ae5a55680657abb70066615739682351589cda67c3f4f"},
{file = "drf_jsonschema_serializer-2.0.0-py3-none-any.whl", hash = "sha256:02b4e4f19d680b0565a4231f1cba29371c01af0e1682944f1e2d1093bbd07d86"},
{file = "drf_jsonschema_serializer-3.0.0-py3-none-any.whl", hash = "sha256:d0e5cce095a5638b0bb7867aa060ed59ab9eed2f54ba5058dd9b483c9c887ed5"},
{file = "drf_jsonschema_serializer-3.0.0.tar.gz", hash = "sha256:8a42c6079225f789cd55321897073b576d15db3406d008e92f44febb017a232a"},
]
[package.dependencies]
django = ">=3.2"
django = ">=4.2"
djangorestframework = ">=3.13"
jsonschema = ">=4.0.0"
@ -1443,7 +1443,7 @@ jsonschema = ">=4.0.0"
all-format-validators = ["fqdn", "idna", "isoduration", "jsonpointer", "rfc3339-validator", "rfc3987", "uri-template", "webcolors"]
coverage = ["pytest-cov"]
docs = ["sphinx", "sphinx-rtd-theme"]
release = ["bump2version", "twine"]
release = ["bump-my-version", "twine"]
tests = ["black", "django-stubs[compatible-mypy]", "djangorestframework-stubs[compatible-mypy]", "flake8", "fqdn", "idna", "isoduration", "isort", "jsonpointer", "mypy", "pytest", "pytest-django", "rfc3339-validator", "rfc3987", "tox", "types-jsonschema", "uri-template", "webcolors"]
[[package]]
@ -3073,13 +3073,13 @@ files = [
[[package]]
name = "pdoc"
version = "14.5.0"
version = "14.5.1"
description = "API Documentation for Python Projects"
optional = false
python-versions = ">=3.8"
files = [
{file = "pdoc-14.5.0-py3-none-any.whl", hash = "sha256:9a8a84e19662610c0620fbe9f2e4174e3b090f8b601ed46348786ebb7517c508"},
{file = "pdoc-14.5.0.tar.gz", hash = "sha256:79f534dc8a6494638dd6056b78e17a654df7ed34cc92646553ce3a7ba5a4fa4a"},
{file = "pdoc-14.5.1-py3-none-any.whl", hash = "sha256:fda6365a06e438b43ca72235b58a2e2ecd66445fcc444313f6ebbde4b0abd94b"},
{file = "pdoc-14.5.1.tar.gz", hash = "sha256:4ddd9c5123a79f511cedffd7231bf91a6e0bd0968610f768342ec5d00b5eefee"},
]
[package.dependencies]
@ -4892,13 +4892,13 @@ files = [
[[package]]
name = "webauthn"
version = "2.1.0"
version = "2.2.0"
description = "Pythonic WebAuthn"
optional = false
python-versions = "*"
files = [
{file = "webauthn-2.1.0-py3-none-any.whl", hash = "sha256:9e1cf916e5ed7c01d54a6dfcc19dacbd2b87b81d2648f001b1fcbcb7aa2ff130"},
{file = "webauthn-2.1.0.tar.gz", hash = "sha256:b196a4246c2818820857ba195c6e6e5398c761117f2269e3d2deab11c7995fc4"},
{file = "webauthn-2.2.0-py3-none-any.whl", hash = "sha256:e8e2daace85dde8f6fb436c1bca9aa72d5931dac8829ecc1562cc4e7cc169f6c"},
{file = "webauthn-2.2.0.tar.gz", hash = "sha256:70e4f318d293125e3a8609838be0561119f4f8846bc430d524f8da4052ee18cc"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.4.2"
version = "2024.6.0"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.4.2
version: 2024.6.0
description: Making authentication simple.
contact:
email: hello@goauthentik.io

View File

@ -6,7 +6,7 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^126.0.3"
"chromedriver": "^126.0.4"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
@ -2084,9 +2084,9 @@
}
},
"node_modules/chromedriver": {
"version": "126.0.3",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-126.0.3.tgz",
"integrity": "sha512-4o+ZK8926/8lqIlnnvcljCHV88Z8IguEMB5PInOiS9/Lb6cyeZSj2Uvz+ky1Jgyw2Bn7qCLJFfbUslaWnvUUbg==",
"version": "126.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-126.0.4.tgz",
"integrity": "sha512-mIdJqdocfN/y9fl5BymIzM9WQLy64x078i5tS1jGFzbFAwXwXrj3zmA86Wf3R/hywPYpWqwXxFGBJHgqZTuGCA==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",

View File

@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^126.0.3"
"chromedriver": "^126.0.4"
}
}

13083
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"lint:precommit": "bun scripts/eslint-precommit.mjs",
"lint:spelling": "node scripts/check-spelling.mjs",
"lit-analyse": "lit-analyzer src",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
"prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling",
"prettier-check": "prettier --check .",
@ -27,7 +28,10 @@
"tsc": "run-s build-locales tsc:execute",
"storybook": "storybook dev -p 6006",
"storybook:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' storybook build",
"storybook:build-import-map": "node scripts/build-storybook-import-maps.mjs"
"storybook:build-import-map": "node scripts/build-storybook-import-maps.mjs",
"test": "npx wdio run ./wdio.conf.ts --logLevel=warn --autoCompileOpts.tsNodeOpts.project=tsconfig.test.json",
"test-view": "npx wdio run ./wdio.conf.ts --autoCompileOpts.tsNodeOpts.project=tsconfig.test.json",
"test-watch": "npx wdio run ./wdio.conf.ts --autoCompileOpts.tsNodeOpts.project=tsconfig.test.json --watch"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.9",
@ -38,7 +42,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.2-1718378698",
"@goauthentik/api": "^2024.6.0-1719407664",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",
@ -46,7 +50,7 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^3.0.2",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^8.11.0",
"@sentry/browser": "^8.12.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.3",
@ -94,8 +98,12 @@
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "1.5.2",
"@types/showdown": "^2.0.6",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@typescript-eslint/eslint-plugin": "^7.14.0",
"@typescript-eslint/parser": "^7.14.0",
"@wdio/browser-runner": "^8.36.1",
"@wdio/cli": "^8.36.1",
"@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.1",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tsconfig-paths": "^1.0.3",
"chokidar": "^3.6.0",
@ -120,10 +128,12 @@
"storybook": "^8.1.10",
"storybook-addon-mock": "^5.0.0",
"ts-lit-plugin": "^2.0.2",
"ts-node": "^10.9.2",
"tslib": "^2.6.3",
"turnstile-types": "^1.2.1",
"typescript": "^5.5.2",
"vite-tsconfig-paths": "^4.3.2"
"vite-tsconfig-paths": "^4.3.2",
"wdio-wait-for": "^3.0.11"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.21.4",

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.4.2";
export const VERSION = "2024.6.0";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -77,7 +77,8 @@ export class AKElement extends LitElement {
}
async getTheme(): Promise<UiThemeEnum> {
return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
// return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
return rootInterface()?.getTheme() || UiThemeEnum.Light;
}
fixElementStyles() {
@ -90,12 +91,9 @@ export class AKElement extends LitElement {
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash
// when dark is preferred
this._activateTheme(
root,
window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
? UiThemeEnum.Light
: UiThemeEnum.Dark,
);
// const pref = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches ? UiThemeEnum.Light : UiThemeEnum.Dark;
// this._activateTheme(root, pref);
this._activateTheme(root, UiThemeEnum.Light);
this._applyTheme(root, await this.getTheme());
}

View File

@ -0,0 +1,63 @@
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { TemplateResult, html, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "./EmptyState.js";
const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase),
ensureCSSStyleSheet(AKGlobal),
];
return litRender(body, document.body);
};
describe("ak-empty-state", () => {
it("should render the default loader", async () => {
render(html`<ak-empty-state ?loading=${true} header=${msg("Loading")}> </ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("Loading");
});
it("should handle standard boolean", async () => {
render(html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("Loading");
});
it("should render a static empty state", async () => {
render(html`<ak-empty-state header=${msg("No messages found")}> </ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
await expect(empty).toHaveClass("fa-question-circle");
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("No messages found");
});
it("should render a slotted message", async () => {
render(
html`<ak-empty-state header=${msg("No messages found")}>
<p slot="body">Try again with a different filter</p>
</ak-empty-state>`,
);
const message = await $("ak-empty-state").$(">>>.pf-c-empty-state__body").$(">>>p");
await expect(message).toHaveText("Try again with a different filter");
});
});

View File

@ -51,11 +51,6 @@ export class AutosubmitStage extends BaseStage<
/>`;
})}
<ak-empty-state ?loading="${true}"> </ak-empty-state>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">

View File

@ -1,9 +1,9 @@
import { html } from "lit";
import "./ApplicationEmptyState";
import "./ak-library-application-empty-list";
export default {
title: "ApplicationEmptyState",
title: "Elements / Application Empty State",
};
export const OrdinaryUser = () =>

View File

@ -1,155 +0,0 @@
import { groupBy } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/user/LibraryApplication";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import styles from "./LibraryPageImpl.css";
import type { Application } from "@goauthentik/api";
import "./ApplicationEmptyState";
import "./ApplicationList";
import "./ApplicationSearch";
import { appHasLaunchUrl } from "./LibraryPageImpl.utils";
import { SEARCH_ITEM_SELECTED, SEARCH_UPDATED } from "./constants";
import { isCustomEvent, loading } from "./helpers";
import type { AppGroupList, PageUIConfig } from "./types";
/**
* List of Applications available
*
* Properties:
* apps: a list of the applications available to the user.
*
* Aggregates two functions:
* - Display the list of applications available to the user
* - Filter that list using the search bar
*
*/
@customElement("ak-library-impl")
export class LibraryPage extends AKElement {
static get styles() {
return styles;
}
@property({ attribute: "isadmin", type: Boolean })
isAdmin = false;
@property({ attribute: false, type: Array })
apps!: Application[];
@property({ attribute: false })
uiConfig!: PageUIConfig;
@state()
selectedApp?: Application;
@state()
filteredApps: Application[] = [];
constructor() {
super();
this.searchUpdated = this.searchUpdated.bind(this);
this.launchRequest = this.launchRequest.bind(this);
}
pageTitle(): string {
return msg("My Applications");
}
connectedCallback() {
super.connectedCallback();
this.filteredApps = this.apps;
if (this.filteredApps === undefined) {
throw new Error(
"Application.results should never be undefined when passed to the Library Page.",
);
}
this.addEventListener(SEARCH_UPDATED, this.searchUpdated);
this.addEventListener(SEARCH_ITEM_SELECTED, this.launchRequest);
}
disconnectedCallback() {
this.removeEventListener(SEARCH_UPDATED, this.searchUpdated);
this.removeEventListener(SEARCH_ITEM_SELECTED, this.launchRequest);
super.disconnectedCallback();
}
searchUpdated(event: Event) {
if (!isCustomEvent(event)) {
throw new Error("ak-library-search-updated must send a custom event.");
}
event.stopPropagation();
const apps = event.detail.apps;
this.selectedApp = undefined;
this.filteredApps = this.apps;
if (apps.length > 0) {
this.selectedApp = apps[0];
this.filteredApps = event.detail.apps;
}
}
launchRequest(event: Event) {
if (!isCustomEvent(event)) {
throw new Error("ak-library-item-selected must send a custom event");
}
event.stopPropagation();
const location = this.selectedApp?.launchUrl;
if (location) {
window.location.assign(location);
}
}
getApps(): AppGroupList {
return groupBy(this.filteredApps.filter(appHasLaunchUrl), (app) => app.group || "");
}
renderEmptyState() {
return html`<ak-library-application-empty-list
?isadmin=${this.isAdmin}
></ak-library-application-empty-list>`;
}
renderApps() {
const selected = this.selectedApp?.slug;
const apps = this.getApps();
const layout = this.uiConfig.layout as string;
const background = this.uiConfig.background;
return html`<ak-library-application-list
layout="${layout}"
background="${ifDefined(background)}"
selected="${ifDefined(selected)}"
.apps=${apps}
></ak-library-application-list>`;
}
renderSearch() {
return html`<ak-library-list-search .apps=${this.apps}></ak-library-list-search>`;
}
render() {
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<div class="pf-c-content header">
<h1 role="heading" aria-level="1" id="library-page-title">
${msg("My applications")}
</h1>
${this.uiConfig.searchEnabled ? this.renderSearch() : html``}
</div>
<section class="pf-c-page__main-section">
${loading(
this.apps,
html`${this.filteredApps.find(appHasLaunchUrl)
? this.renderApps()
: this.renderEmptyState()}`,
)}
</section>
</main>`;
}
}

View File

@ -12,7 +12,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Application } from "@goauthentik/api";
import type { AppGroupEntry, AppGroupList } from "./types";
import type { AppGroupEntry, AppGroupList } from "./types.js";
type Pair = [string, string];
@ -31,6 +31,13 @@ const LAYOUTS = new Map<string, [string, string]>([
],
]);
/**
* @element ak-library-application-list
* @class LibraryPageApplicationList
*
* Renders the current library list of a User's Applications.
*
*/
@customElement("ak-library-application-list")
export class LibraryPageApplicationList extends AKElement {
static get styles() {

View File

@ -0,0 +1,33 @@
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
/**
* Library Page Application List Empty
*
* Display a message if there are no applications defined in the current instance. If the user is an
* administrator, provide a link to the "Create a new application" page.
*/
@customElement("ak-library-application-search-empty")
export class LibraryPageApplicationSearchEmpty extends AKElement {
static get styles() {
return [PFBase, PFEmptyState, PFContent, PFSpacing];
}
render() {
return html` <div class="pf-c-empty-state pf-m-full-height">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">${msg("Search returned no results.")}</h1>
</div>
</div>`;
}
}

View File

@ -13,11 +13,30 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import type { Application } from "@goauthentik/api";
import { SEARCH_ITEM_SELECTED, SEARCH_UPDATED } from "./constants";
import { customEvent } from "./helpers";
import {
LibraryPageSearchEmpty,
LibraryPageSearchReset,
LibraryPageSearchSelected,
LibraryPageSearchUpdated,
} from "./events.js";
@customElement("ak-library-list-search")
export class LibraryPageApplicationList extends AKElement {
/**
* @element ak-library-list-search
*
* @class LibraryPageApplicationSearch
*
* @classdesc
*
* The interface between our list of applications shown to the user, an input box, and the Fuse
* fuzzy search library.
*
* @fires LibraryPageSearchUpdated
* @fires LibraryPageSearchEmpty
* @fires LibraryPageSearchReset
*
*/
@customElement("ak-library-application-search")
export class LibraryPageApplicationSearch extends AKElement {
static get styles() {
return [
PFBase,
@ -75,11 +94,7 @@ export class LibraryPageApplicationList extends AKElement {
}
onSelected(apps: FuseResult<Application>[]) {
this.dispatchEvent(
customEvent(SEARCH_UPDATED, {
apps: apps.map((app) => app.item),
}),
);
this.dispatchEvent(new LibraryPageSearchUpdated(apps.map((app) => app.item)));
}
connectedCallback() {
@ -102,7 +117,7 @@ export class LibraryPageApplicationList extends AKElement {
updateURLParams({
search: this.query,
});
this.onSelected([]);
this.dispatchEvent(new LibraryPageSearchReset());
}
onInput(ev: InputEvent) {
@ -113,8 +128,13 @@ export class LibraryPageApplicationList extends AKElement {
updateURLParams({
search: this.query,
});
const apps = this.fuse.search(this.query);
if (apps.length < 1) return;
if (apps.length < 1) {
this.dispatchEvent(new LibraryPageSearchEmpty());
return;
}
this.onSelected(apps);
}
@ -125,7 +145,7 @@ export class LibraryPageApplicationList extends AKElement {
return;
}
case "Enter": {
this.dispatchEvent(customEvent(SEARCH_ITEM_SELECTED));
this.dispatchEvent(new LibraryPageSearchSelected());
return;
}
}

View File

@ -0,0 +1,182 @@
import { groupBy } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import "@goauthentik/user/LibraryApplication";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import styles from "./LibraryPageImpl.css";
import type { Application } from "@goauthentik/api";
import "./ak-library-application-empty-list.js";
import "./ak-library-application-list.js";
import "./ak-library-application-search-empty.js";
import "./ak-library-application-search.js";
import {
LibraryPageSearchEmpty,
LibraryPageSearchReset,
LibraryPageSearchSelected,
LibraryPageSearchUpdated,
} from "./events.js";
import type { PageUIConfig } from "./types.js";
/**
* List of Applications available
*
* Properties:
* apps: a list of the applications available to the user.
*
* Aggregates two functions:
* - Display the list of applications available to the user
* - Filter that list using the search bar
*
*/
@customElement("ak-library-impl")
export class LibraryPage extends AKElement {
static get styles() {
return styles;
}
/**
* Controls showing the "Switch to Admin" button.
*
* @attr
*/
@property({ attribute: "isadmin", type: Boolean })
isAdmin = false;
/**
* The *complete* list of applications for this user. Not paginated.
*
* @attr
*/
@property({ attribute: false, type: Array })
apps!: Application[];
/**
* The aggregate uiConfig, derived from user, brand, and instance data.
*
* @attr
*/
@property({ attribute: false })
uiConfig!: PageUIConfig;
@state()
selectedApp?: Application;
@state()
filteredApps: Application[] = [];
pageTitle(): string {
return msg("My Applications");
}
connectedCallback() {
super.connectedCallback();
this.filteredApps = this.apps;
if (this.filteredApps === undefined) {
throw new Error(
"Application.results should never be undefined when passed to the Library Page.",
);
}
this.addEventListener(LibraryPageSearchUpdated.eventName, this.searchUpdated);
this.addEventListener(LibraryPageSearchReset.eventName, this.searchReset);
this.addEventListener(LibraryPageSearchEmpty.eventName, this.searchEmpty);
this.addEventListener(LibraryPageSearchSelected.eventName, this.launchRequest);
}
disconnectedCallback() {
this.removeEventListener(LibraryPageSearchUpdated.eventName, this.searchUpdated);
this.removeEventListener(LibraryPageSearchReset.eventName, this.searchReset);
this.removeEventListener(LibraryPageSearchEmpty.eventName, this.searchEmpty);
this.removeEventListener(LibraryPageSearchSelected.eventName, this.launchRequest);
super.disconnectedCallback();
}
@bound
searchUpdated(event: LibraryPageSearchUpdated) {
event.stopPropagation();
const apps = event.apps;
if (!(apps.length > 0)) {
throw new Error(
"LibaryPageSearchUpdated had empty results body. This must not happen.",
);
}
this.filteredApps = apps;
this.selectedApp = apps[0];
}
@bound
launchRequest(event: LibraryPageSearchSelected) {
event.stopPropagation();
this.selectedApp?.launchUrl && window.location.assign(this.selectedApp?.launchUrl);
}
@bound
searchReset(event: LibraryPageSearchReset) {
event.stopPropagation();
this.filteredApps = this.apps;
this.selectedApp = undefined;
}
@bound
searchEmpty(event: LibraryPageSearchEmpty) {
event.stopPropagation();
this.filteredApps = [];
this.selectedApp = undefined;
}
renderApps() {
const selected = this.selectedApp?.slug;
const layout = this.uiConfig.layout as string;
const background = this.uiConfig.background;
const groupedApps = groupBy(this.filteredApps, (app) => app.group || "");
return html`<ak-library-application-list
layout="${layout}"
background="${ifDefined(background)}"
selected="${ifDefined(selected)}"
.apps=${groupedApps}
></ak-library-application-list>`;
}
renderSearch() {
return html`<ak-library-application-search
.apps=${this.apps}
></ak-library-application-search>`;
}
renderSearchEmpty() {
return nothing;
}
renderState() {
if (this.apps.length === 0) {
return html`<ak-library-application-empty-list
?isadmin=${this.isAdmin}
></ak-library-application-empty-list>`;
}
if (this.filteredApps.length === 0) {
return html`<ak-library-application-search-empty></ak-library-application-search-empty>`;
}
return this.renderApps();
}
render() {
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<div class="pf-c-content header">
<h1 role="heading" aria-level="1" id="library-page-title">
${msg("My applications")}
</h1>
${this.uiConfig.searchEnabled ? this.renderSearch() : nothing}
</div>
<section class="pf-c-page__main-section">${this.renderState()}</section>
</main>`;
}
}

View File

@ -9,8 +9,8 @@ import { customElement, state } from "lit/decorators.js";
import { Application, CoreApi } from "@goauthentik/api";
import "./LibraryPageImpl";
import type { PageUIConfig } from "./types";
import "./ak-library-impl.js";
import type { PageUIConfig } from "./types.js";
/**
* List of Applications available
@ -35,6 +35,10 @@ export class LibraryPage extends AKElement {
@state()
isAdmin = false;
/**
* The list of applications. This is the *complete* list; the constructor fetches as many pages
* as the server announces when page one is accessed, and then concatenates them all together.
*/
@state()
apps: Application[] = [];

View File

@ -1,2 +0,0 @@
export const SEARCH_UPDATED = "authentik.search-updated";
export const SEARCH_ITEM_SELECTED = "authentik.search-item-selected";

View File

@ -0,0 +1,72 @@
import type { Application } from "@goauthentik/api";
/**
* @class LibraryPageSearchUpdated
*
* Indicates that the user has made a query that resulted in some
* applications being filtered-for.
*
*/
export class LibraryPageSearchUpdated extends Event {
static readonly eventName = "authentik.library.search-updated";
/**
* @attr apps: The list of those entries found by the current search.
*/
constructor(public apps: Application[]) {
super(LibraryPageSearchUpdated.eventName, { composed: true, bubbles: true });
}
}
/**
* @class LibraryPageSearchReset
*
* Indicates that the user has emptied the search field. Intended to
* signal that all available apps are to be displayed.
*
*/
export class LibraryPageSearchReset extends Event {
static readonly eventName = "authentik.library.search-reset";
constructor() {
super(LibraryPageSearchReset.eventName, { composed: true, bubbles: true });
}
}
/**
* @class LibraryPageSearchEmpty
*
* Indicates that the user has made a query that resulted in an empty
* list being returned. Intended to signal that an alternative "No
* matching applications found" message be displayed.
*
*/
export class LibraryPageSearchEmpty extends Event {
static readonly eventName = "authentik.library.search-empty";
constructor() {
super(LibraryPageSearchEmpty.eventName, { composed: true, bubbles: true });
}
}
/**
* @class LibraryPageSearchEmpty
*
* Indicates that the user has pressed "Enter" while focused on the
* search box. Intended to signal that the currently highlighted search
* entry (if any) should be activated.
*
*/
export class LibraryPageSearchSelected extends Event {
static readonly eventName = "authentik.library.search-item-selected";
constructor() {
super(LibraryPageSearchSelected.eventName, { composed: true, bubbles: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
[LibraryPageSearchUpdated.eventName]: LibraryPageSearchUpdated;
[LibraryPageSearchReset.eventName]: LibraryPageSearchReset;
[LibraryPageSearchEmpty.eventName]: LibraryPageSearchEmpty;
[LibraryPageSearchSelected.eventName]: LibraryPageSearchSelected;
}
}

View File

@ -1,24 +0,0 @@
import "@goauthentik/elements/EmptyState";
import { msg } from "@lit/localize";
import { html } from "lit";
import type { TemplateResult } from "lit";
export const customEvent = (name: string, details = {}) =>
new CustomEvent(name as string, {
composed: true,
bubbles: true,
detail: details,
});
// "Unknown" seems to violate some obscure Typescript rule and doesn't work here, although it
// should.
//
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isCustomEvent = (v: any): v is CustomEvent =>
v instanceof CustomEvent && "detail" in v;
export const loading = <T>(v: T, actual: TemplateResult) =>
v
? actual
: html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> </ak-empty-state>`;

View File

@ -1,5 +1,5 @@
import { Route } from "@goauthentik/elements/router/Route";
import "@goauthentik/user/LibraryPage/LibraryPage";
import "@goauthentik/user/LibraryPage/ak-library.js";
import { html } from "lit";

View File

@ -2,16 +2,16 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"paths": {
"@goauthentik/admin/*": ["src/admin/*"],
"@goauthentik/common/*": ["src/common/*"],
"@goauthentik/components/*": ["src/components/*"],
"@goauthentik/admin/*": ["./src/admin/*"],
"@goauthentik/common/*": ["./src/common/*"],
"@goauthentik/components/*": ["./src/components/*"],
"@goauthentik/docs/*": ["../website/docs/*"],
"@goauthentik/elements/*": ["src/elements/*"],
"@goauthentik/flow/*": ["src/flow/*"],
"@goauthentik/locales/*": ["src/locales/*"],
"@goauthentik/polyfill/*": ["src/polyfill/*"],
"@goauthentik/standalone/*": ["src/standalone/*"],
"@goauthentik/user/*": ["src/user/*"]
},
"@goauthentik/elements/*": ["./src/elements/*"],
"@goauthentik/flow/*": ["./src/flow/*"],
"@goauthentik/locales/*": ["./src/locales/*"],
"@goauthentik/polyfill/*": ["./src/polyfill/*"],
"@goauthentik/standalone/*": ["./src/standalone/*"],
"@goauthentik/user/*": ["./src/user/*"]
}
}
}

34
web/tsconfig.test.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"baseUrl": ".",
"types": ["node", "webdriverio/async", "@wdio/cucumber-framework", "expect-webdriverio"],
"target": "esnext",
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"paths": {
"@goauthentik/admin/*": ["./src/admin/*"],
"@goauthentik/common/*": ["./src/common/*"],
"@goauthentik/components/*": ["./src/components/*"],
"@goauthentik/docs/*": ["../website/docs/*"],
"@goauthentik/elements/*": ["./src/elements/*"],
"@goauthentik/flow/*": ["./src/flow/*"],
"@goauthentik/locales/*": ["./src/locales/*"],
"@goauthentik/polyfill/*": ["./src/polyfill/*"],
"@goauthentik/standalone/*": ["./src/standalone/*"],
"@goauthentik/user/*": ["./src/user/*"]
}
}
}

347
web/wdio.conf.ts Normal file
View File

@ -0,0 +1,347 @@
import replace from "@rollup/plugin-replace";
import type { Options } from "@wdio/types";
import { cwd } from "process";
// @ts-ignore
import * as modify from "rollup-plugin-modify";
import * as postcssLit from "rollup-plugin-postcss-lit";
import type { UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { cssImportMaps } from "./.storybook/css-import-maps";
const isProdBuild = process.env.NODE_ENV === "production";
const apiBasePath = process.env.AK_API_BASE_PATH || "";
const runHeadless = process.env.CI !== undefined;
export const config: Options.Testrunner = {
//
// ====================
// Runner Configuration
// ====================
// WebdriverIO supports running e2e tests as well as unit and component tests.
runner: [
"browser",
{
viteConfig: (config: UserConfig = { plugins: [] }) => ({
...config,
plugins: [
modify(cssImportMaps),
replace({
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development",
),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
"preventAssignment": true,
}),
...(config?.plugins ?? []),
// @ts-ignore
postcssLit(),
tsconfigPaths(),
],
}),
},
],
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
project: "./tsconfig.json",
transpileOnly: true,
},
},
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// of the configuration file being run.
//
// The specs are defined as an array of spec files (optionally using wildcards
// that will be expanded). The test for each spec file will be run in a separate
// worker process. In order to have a group of spec files run in the same worker
// process simply enclose them in an array within the specs array.
//
// The path of the spec files will be resolved relative from the directory of
// of the config file unless it's absolute.
//
specs: ["./src/**/*.test.ts"],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://saucelabs.com/platform/platform-configurator
//
capabilities: [
{
// capabilities for local browser web tests
browserName: "chrome", // or "firefox", "microsoftedge", "safari"
...(runHeadless
? {
"goog:chromeOptions": {
args: ["headless", "disable-gpu"],
},
}
: {}),
},
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: "info",
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'info',
// '@wdio/appium-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
// gets prepended directly.
// baseUrl: 'http://localhost:8080',
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 120000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Test runner services
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
// services: [],
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: "mocha",
//
// The number of times to retry the entire specfile when it fails as a whole
// specFileRetries: 1,
//
// Delay in seconds between the spec file retry attempts
// specFileRetriesDelay: 0,
//
// Whether or not retried spec files should be retried immediately or deferred to the end of the queue
// specFileRetriesDeferred: false,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter
reporters: ["spec"],
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: "bdd",
timeout: 60000,
},
//
// =====
// Hooks
// =====
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
// it and to build services around it. You can either apply a single function or an array of
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
// resolved to continue.
/**
* Gets executed once before all workers get launched.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
*/
// onPrepare: function (config, capabilities) {
// },
/**
* Gets executed before a worker process is spawned and can be used to initialize specific service
* for that worker as well as modify runtime environments in an async fashion.
* @param {string} cid capability id (e.g 0-0)
* @param {object} caps object containing capabilities for session that will be spawn in the worker
* @param {object} specs specs to be run in the worker process
* @param {object} args object that will be merged with the main configuration once worker is initialized
* @param {object} execArgv list of string arguments passed to the worker process
*/
// onWorkerStart: function (cid, caps, specs, args, execArgv) {
// },
/**
* Gets executed just after a worker process has exited.
* @param {string} cid capability id (e.g 0-0)
* @param {number} exitCode 0 - success, 1 - fail
* @param {object} specs specs to be run in the worker process
* @param {number} retries number of retries used
*/
// onWorkerEnd: function (cid, exitCode, specs, retries) {
// },
/**
* Gets executed just before initialising the webdriver session and test framework. It allows you
* to manipulate configurations depending on the capability or spec.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {string} cid worker id (e.g. 0-0)
*/
// beforeSession: function (config, capabilities, specs, cid) {
// },
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {object} browser instance of created browser/device session
*/
// before: function (capabilities, specs) {
// },
/**
* Runs before a WebdriverIO command gets executed.
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
*/
// beforeCommand: function (commandName, args) {
// },
/**
* Hook that gets executed before the suite starts
* @param {object} suite suite details
*/
// beforeSuite: function (suite) {
// },
/**
* Function to be executed before a test (in Mocha/Jasmine) starts.
*/
// beforeTest: function (test, context) {
// },
/**
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
* beforeEach in Mocha)
*/
// beforeHook: function (test, context, hookName) {
// },
/**
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
* afterEach in Mocha)
*/
// afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) {
// },
/**
* Function to be executed after a test (in Mocha/Jasmine only)
* @param {object} test test object
* @param {object} context scope object the test was executed with
* @param {Error} result.error error object in case the test fails, otherwise `undefined`
* @param {*} result.result return object of test function
* @param {number} result.duration duration of test
* @param {boolean} result.passed true if test has passed, otherwise false
* @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }`
*/
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
// },
/**
* Hook that gets executed after the suite has ended
* @param {object} suite suite details
*/
// afterSuite: function (suite) {
// },
/**
* Runs after a WebdriverIO command gets executed
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {number} result 0 - command success, 1 - command error
* @param {object} error error object if any
*/
// afterCommand: function (commandName, args, result, error) {
// },
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// after: function (result, capabilities, specs) {
// },
/**
* Gets executed right after terminating the webdriver session.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// afterSession: function (config, capabilities, specs) {
// },
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {object} exitCode 0 - success, 1 - fail
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
// onComplete: function(exitCode, config, capabilities, results) {
// },
/**
* Gets executed when a refresh happens.
* @param {string} oldSessionId session ID of the old session
* @param {string} newSessionId session ID of the new session
*/
// onReload: function(oldSessionId, newSessionId) {
// }
/**
* Hook that gets executed before a WebdriverIO assertion happens.
* @param {object} params information about the assertion to be executed
*/
// beforeAssertion: function(params) {
// }
/**
* Hook that gets executed after a WebdriverIO assertion happened.
* @param {object} params information about the assertion that was executed, including its results
*/
// afterAssertion: function(params) {
// }
};

View File

@ -6689,6 +6689,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -6955,6 +6955,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -6606,6 +6606,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8801,6 +8801,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8535,6 +8535,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8379,6 +8379,9 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8805,6 +8805,9 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8650,4 +8650,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body></file></xliff>

View File

@ -6599,6 +6599,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -5521,6 +5521,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@ -596,9 +596,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>"。</target>
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@ -1040,8 +1040,8 @@
</trans-unit>
<trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="s55787f4dfcdce52b">
@ -1767,8 +1767,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@ -2946,8 +2946,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit>
<trans-unit id="s026555347e589f0e">
@ -3708,8 +3708,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target>
</trans-unit>
<trans-unit id="s44536d20bb5c8257">
@ -3885,10 +3885,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@ -4964,7 +4964,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A "roaming" authenticator, like a YubiKey</source>
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@ -5299,10 +5299,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target>
<x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
<x id="0" equiv-text="${prompt.name}"/>&quot;
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为
<x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit>
@ -5351,7 +5351,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@ -7795,7 +7795,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
</trans-unit>
<trans-unit id="s824e0943a7104668">
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca">
@ -8807,7 +8807,11 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
<target>FIPS 状态</target>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
<target>搜索未返回结果。</target>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@ -6647,6 +6647,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -8807,6 +8807,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
<target>FIPS 状态</target>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
<target>搜索未返回结果。</target>
</trans-unit>
</body>
</file>

View File

@ -8496,6 +8496,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s0b2ad58c3deaa8dd">
<source>FIPS Status</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
</body>
</file>

View File

@ -3,20 +3,18 @@ title: Welcome to authentik
slug: /
---
## About authentik documentation
Our tech docs cover the typical topics, from installation to configuration, adding providers, defining policies and creating login flows, event monitoring, security, and attributes. [Enterprise](./enterprise/index.md) version documentation is included here, within our standard tech docs.
- For information about integrating a specific application or software into authentik, refer to our Integrations section, accessible from the top menu bar.
- For developer-focused documentation, such as using our APIs and blueprints, setting up your development environment, translations, or how to contribute, refer to the [Developer](../developer-docs) area, accessible from the top menu bar.
## What is authentik?
authentik is an open-source Identity Provider, focused on flexibility and versatility. With authentik, site administrators, application developers, and security engineers have a dependable and secure solution for authentication in almost any type of environment. There are robust recovery actions available for the users and applications, including user profile and password management. You can quickly edit, deactivate, or even impersonate a user profile, and set a new password for new users or reset an existing password.
authentik is an IdP (Identity Provider) and SSO (single sign on) that is built with security at the forefront of every piece of code, every feature, with an emphasis on flexibility and versatility.
With authentik, site administrators, application developers, and security engineers have a dependable and secure solution for authentication in almost any type of environment. There are robust recovery actions available for the users and applications, including user profile and password management. You can quickly edit, deactivate, or even impersonate a user profile, and set a new password for new users or reset an existing password.
You can use authentik in an existing environment to add support for new protocols, so introducing authentik to your current tech stack doesn't present re-architecting challenges. We support all of the major providers, such as OAuth2, SAML, LDAP, and SCIM, so you can pick the protocol that you need for each application.
We offer two versions of authentik: the forever-free open source project upon which everything is built, and our open core, source available Enterprise version, with a Support center and additional features.
## User-focused & task-oriented dashboards
The authentik product provides the following consoles:
- **Admin interface**: a visual tool for the creation and management of users and groups, tokens and credentials, application integrations, events, and the Flows that define standard and customizable login and authentication processes. Easy-to-read visual dashboards display system status, recent logins and authentication events, and application usage.
@ -25,15 +23,7 @@ The authentik product provides the following consoles:
- **Flows**: [_Flows_](./flow) are the steps by which the various _Stages_ of a login and authentication process occurs. A stage represents a single verification or logic step in the sign-on process. authentik allows for the customization and exact definition of these flows.
## Installation
Refer to the installation steps in either [Docker-compose](installation/docker-compose) or [Kubernetes](installation/kubernetes).
For more information about configuration, beta versions, and additional installation options, see our main [Installation](installation) section.
## Screenshots
authentik can use Light or Dark mode for the Admin interface, User interface and the Flow interface.
In authentik, you can use Light or Dark mode for the Admin interface, User interface, and the Flow interface.
import "react-before-after-slider-component/dist/build.css";
import ReactBeforeSliderComponent from "react-before-after-slider-component";
@ -71,3 +61,17 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
imageUrl: useBaseUrl("img/screen_admin_light.jpg"),
}}
/>
## Documentation
Our tech docs cover the typical topics, from installation to configuration, adding providers, defining policies and creating login flows, event monitoring, security, and attributes. [Enterprise](./enterprise/index.md) version documentation is included here, within our standard tech docs.
- For information about integrating a specific application or software into authentik, refer to our **Integrations** section, accessible from the top menu bar.
- For developer-focused documentation, such as using our APIs and blueprints, setting up your development environment, translations, or how to contribute, refer to the [**Developer**](../developer-docs) area, accessible from the top menu bar.
## Installation
Refer to the installation steps in either [Docker-compose](installation/docker-compose) or [Kubernetes](installation/kubernetes).
For more information about configuration, beta versions, and additional installation options, see our main [Installation](installation) section.

View File

@ -50,8 +50,8 @@ Run the following commands to generate a password and secret key and write them
{/* prettier-ignore */}
```shell
echo "PG_PASS=$(openssl rand 36 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64)" >> .env
echo "PG_PASS=$(openssl rand 36 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64 -w 0)" >> .env
```
:::info

View File

@ -23,7 +23,7 @@ Start by generating passwords for the database and cache. You can use either of
```shell
pwgen -s 50 1
openssl rand 60 | base64
openssl rand 60 | base64 -w 0
```
### Set Values

View File

@ -53,16 +53,26 @@ import Objects from "../expressions/_objects.md";
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
- `geoip`: GeoIP object, see [GeoIP](https://geoip2.readthedocs.io/en/latest/#geoip2.models.City)
- `geoip`: GeoIP dictionary. The following fields are available:
- `continent`: a two character continent code like `NA` (North America) or `OC` (Oceania).
- `country`: the two character [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) alpha code for the country.
- `lat`: the approximate latitude of the location associated with the IP address.
- `long`: the approximate longitude of the location associated with the IP address.
- `city`: the name of the city. May be empty.
```python
return context["geoip"].country.iso_code == "US"
return context["geoip"]["country"] == "US"
```
- `asn`: ASN object, see [GeoIP](https://geoip2.readthedocs.io/en/latest/#geoip2.models.ASN)
- `asn`: ASN dictionary. The follow fields are available:
- `asn`: the autonomous system number associated with the IP address.
- `as_org`: the organization associated with the registered autonomous system number for the IP address.
- `network`: the network associated with the record. In particular, this is the largest network where all of the fields except `ip_address` have the same value.
```python
return context["asn"].autonomous_system_number == 64496
return context["asn"]["asn"] == 64496
```
- `ak_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.

View File

@ -347,6 +347,11 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.2
- stages/user_write: ensure user data is json-serializable (cherry-pick #8926) (#8928)
- tenants: really ensure default tenant cannot be deleted (cherry-pick #8875) (#8876)
## Fixed in 2024.2.4
- security: fix [CVE-2024-37905](../../security/CVE-2024-37905.md), reported by [@m2a2](https://github.com/m2a2) (cherry-pick #10230) (#10238)
- security: fix [CVE-2024-38371](../../security/CVE-2024-38371.md), reported by Stefan Zwanenburg (cherry-pick #10229) (#10235)
## API Changes
#### What's New

View File

@ -235,6 +235,14 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.4
- web/flows: fix missing fallback for flow logo (cherry-pick #9487) (#9492)
- web: Add missing integrity hashes to package-lock.json (#9527)
## Fixed in 2024.4.3
- core: fix source flow_manager not always appending save stage (cherry-pick #9659) (#9662)
- security: fix [CVE-2024-37905](../../security/CVE-2024-37905.md), reported by [@m2a2](https://github.com/m2a2) (cherry-pick #10230) (#10236)
- security: fix [CVE-2024-38371](../../security/CVE-2024-38371.md), reported by Stefan Zwanenburg (cherry-pick #10229) (#10233)
- sources/saml: fix FlowPlanner error due to pickle (cherry-pick #9708) (#9709)
- web: fix value handling inside controlled components (cherry-pick #9648) (#9685)
## API Changes
#### What's New

View File

@ -3,12 +3,6 @@ title: Release 2024.6
slug: /releases/2024.6
---
:::::note
2024.6 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2024.6.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
:::::
## Highlights
- **PostgreSQL read replicas**: Optimize database query routing by using read replicas to balance the load
@ -29,6 +23,10 @@ The provided Compose file was updated with PostgreSQL 16. You can follow the pro
With this release, authentik now enforces unique group names. Existing groups with name collisions that were created in earlier versions can still exist, but any new groups you create will need a unique name. If changing attributes, permission-level, or parent on an existing group with a name collision, you need to also change its name to be unique. Note that changing members or roles associated with the group does not require a rename.
### GeoIP and ASN context object
The `context["geoip"]` and `context["asn"]` objects available in expression policies are now dictionaries. Attributes must now be accessed via dictionary accessors. See [our policy examples](../../policies/expression.mdx) for the updated syntax.
## New features
- **Google Workspace Provider** <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>
@ -111,6 +109,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.6
- core: groups: optimize recursive children query (#9931)
- core: include version in built JS files (cherry-pick #9558) (#10148)
- core: only prefetch related objects when required (#9476)
- core: rework base for SkipObject exception to better support control flow exceptions (cherry-pick #10186) (#10187)
- crypto: update fingerprint at same time as certificate (#10036)
- enterprise/audit: fix audit logging with m2m relations (#9571)
- enterprise/providers/google: initial account sync to google workspace (#9384)
@ -152,6 +151,8 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.6
- root: handle asgi exception (#10085)
- root: include task_id in events and logs (#9749)
- root: use custom model serializer that saves m2m without bulk (cherry-pick #10139) (#10151)
- security: fix [CVE-2024-37905](../../security/CVE-2024-37905.md), reported by [@m2a2](https://github.com/m2a2) (cherry-pick #10230) (#10237)
- security: fix [CVE-2024-38371](../../security/CVE-2024-38371.md), reported by Stefan Zwanenburg (cherry-pick #10229) (#10234)
- sources/oauth: ensure all UI sources return a valid source (#9401)
- sources/oauth: fix OAuth Client sending token request incorrectly (#9474)
- sources/oauth: modernizes discord icon (#9817)

View File

@ -0,0 +1,27 @@
# CVE-2024-37905
_Reported by [@m2a2](https://github.com/m2a2)_
## Improper Authorization for Token modification
### Summary
Due to insufficient permission checks it was possible for any authenticated user to elevate their permissions to a superuser by creating an API token and changing the user the token belonged to.
### Patches
authentik 2024.6.0, 2024.4.3 and 2024.2.4 fix this issue, for other versions the workaround can be used.
### Details
By setting a token's user ID to the ID of a higher privileged user, the token will inherit the higher privileged access to the API. This can be used to change the password of the affected user or to modify the authentik configuration in a potentially malicious way.
### Workarounds
As a workaround it is possible to block any requests to `/api/v3/core/tokens*` at the reverse-proxy/load-balancer level. Doing so prevents this issue from being exploited.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -0,0 +1,23 @@
# CVE-2024-38371
_Reported by Stefan Zwanenburg_
## Insufficient access control for OAuth2 Device Code flow
### Impact
Due to a bug, access restrictions assigned to an application were not checked when using the OAuth2 Device code flow. This could potentially allow users without the correct authorization to get OAuth tokens for an application, and access the application.
### Patches
authentik 2024.6.0, 2024.4.3 and 2024.2.4 fix this issue, for other versions the workaround can be used.
### Workarounds
As authentik flows are still used as part of the OAuth2 Device code flow, it is possible to add access control to the configured flows.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -19,7 +19,7 @@ The following placeholders will be used:
![](./01_user_create.png)
3. Give the User a password, generated using for example `pwgen 64 1` or `openssl rand 36 | base64`.
3. Give the User a password, generated using for example `pwgen 64 1` or `openssl rand 36 | base64 -w 0`.
4. Open the Delegation of Control Wizard by right-clicking the domain and selecting "All Tasks".

View File

@ -16,7 +16,7 @@ The following placeholders will be used:
1. Log into FreeIPA.
2. Create a user in FreeIPA, matching your naming scheme. Provide a strong password, example generation methods: `pwgen 64 1` or `openssl rand 36 | base64`. After you are done click **Add and Edit**.
2. Create a user in FreeIPA, matching your naming scheme. Provide a strong password, example generation methods: `pwgen 64 1` or `openssl rand 36 | base64 -w 0`. After you are done click **Add and Edit**.
![](./01_user_create.png)

View File

@ -48,7 +48,7 @@ PAPERLESS_SOCIALACCOUNT_PROVIDERS: >
"provider_id": "authentik",
"name": "Authentik",
"client_id": "< Client ID >",
"secret": "< Client Secret >,
"secret": "< Client Secret >",
"settings": {
"server_url": "https://authentik.company/application/o/paperless/.well-known/openid-configuration"
}

5328
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -510,6 +510,8 @@ const docsSidebar = {
items: [
"security/security-hardening",
"security/policy",
"security/CVE-2024-38371",
"security/CVE-2024-37905",
"security/CVE-2024-23647",
"security/CVE-2024-21637",
"security/CVE-2023-48228",