Compare commits
128 Commits
openapi-ge
...
tests/e2e/
Author | SHA1 | Date | |
---|---|---|---|
4b0d641a51 | |||
99b559893b | |||
8014088c3a | |||
3ee353126f | |||
db76c5d9e2 | |||
61bff69b7d | |||
69651323e3 | |||
75a0ac9588 | |||
941a697397 | |||
4a74db17a1 | |||
0cf6bff93c | |||
814e438422 | |||
2db77a37dd | |||
e40c5ac617 | |||
7440900dac | |||
ca96b27825 | |||
ad4a765a80 | |||
4dcd481010 | |||
d0dc14d84d | |||
7bf960352b | |||
c07d01661b | |||
427597ec14 | |||
7cc77bd387 | |||
381a1a2c49 | |||
08f8222224 | |||
1211c34a18 | |||
22efb57369 | |||
3eeda53be6 | |||
82ace18703 | |||
8589079252 | |||
ae2af6e58e | |||
86a7f98ff6 | |||
3af45371d3 | |||
b01ffd934f | |||
f11ba94603 | |||
7d2aa43364 | |||
f1351a7577 | |||
0611eea0e7 | |||
d0b46fcf9c | |||
dcbdc37d31 | |||
d07f396379 | |||
0972103b83 | |||
b448e76db4 | |||
f2937bd6dd | |||
53c2e3e77c | |||
7dd62c1f55 | |||
33e3510fba | |||
0e5fac2642 | |||
c53b1fe78a | |||
838a7457b2 | |||
a3c07bc9ff | |||
121f2c609d | |||
365affc28e | |||
f367822779 | |||
848198125d | |||
497ac5e3d0 | |||
1773d4d681 | |||
4edbb51939 | |||
c7e97ab48e | |||
31f7faae1c | |||
f5dae2ae92 | |||
2c043dba0b | |||
bda10e5db1 | |||
be9ae7d4f7 | |||
b4a6189bfa | |||
bfdb827ff9 | |||
488a58e1c5 | |||
3f83e69453 | |||
e92fa5df0b | |||
f8c22170df | |||
e3d08a8434 | |||
97d3e9afdc | |||
1eb08def73 | |||
6e3b379e4a | |||
264f59775c | |||
d048f1ecbd | |||
eb31f31584 | |||
fe5c842e92 | |||
b82d3100c9 | |||
49bb668036 | |||
52c70c7700 | |||
b99fd36f86 | |||
8a5381eca3 | |||
2c77830179 | |||
ffcd7def60 | |||
ed121bc2a3 | |||
d5ab9d9167 | |||
a983321ad6 | |||
9c3420ede4 | |||
91b40350aa | |||
1912991682 | |||
71b9117f53 | |||
b5f947f460 | |||
3a2f7e9549 | |||
1582ce0920 | |||
6d3eea5266 | |||
e987208bd1 | |||
0efab8eef7 | |||
9402dac8ae | |||
f57a290eee | |||
5dab0d2b7a | |||
2da6036248 | |||
cdba94cea4 | |||
c59eca664a | |||
d5b205f9c0 | |||
8ad9ad833e | |||
599ce15f68 | |||
91310eff52 | |||
b522d6732a | |||
17d96f204e | |||
65e4667bc3 | |||
f67f9e5ed0 | |||
62dd6a4393 | |||
a46eae8276 | |||
c4acc9fc24 | |||
e748a03082 | |||
e473f28e21 | |||
f70635c295 | |||
70d60c7ab2 | |||
61a26c02b7 | |||
a06645d558 | |||
7730ecbd37 | |||
80e1be8db7 | |||
c528c74e48 | |||
6d7bf36afe | |||
44fb59eb18 | |||
8f8d924935 | |||
602adaa5c5 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.4.0
|
||||
current_version = 2025.4.1
|
||||
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*))?
|
||||
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@ -118,3 +118,15 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
- package-ecosystem: docker-compose
|
||||
directories:
|
||||
# - /scripts # Maybe
|
||||
- /tests/e2e
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
|
3
.github/workflows/ci-main.yml
vendored
3
.github/workflows/ci-main.yml
vendored
@ -200,7 +200,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }}
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
working-directory: web
|
||||
@ -208,6 +208,7 @@ jobs:
|
||||
npm ci
|
||||
make -C .. gen-client-ts
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 5000s --verbose
|
||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -16,7 +16,7 @@
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||
"typescript.tsdk": "./web/node_modules/typescript/lib",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"yaml.schemas": {
|
||||
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
||||
@ -30,7 +30,5 @@
|
||||
}
|
||||
],
|
||||
"go.testFlags": ["-count=1"],
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"]
|
||||
}
|
||||
|
10
Dockerfile
10
Dockerfile
@ -40,7 +40,8 @@ COPY ./web /work/web/
|
||||
COPY ./website /work/website/
|
||||
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||
|
||||
RUN npm run build
|
||||
RUN npm run build && \
|
||||
npm run build:sfe
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
||||
@ -85,18 +86,17 @@ FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
|
||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||
ENV GEOIPUPDATE_VERBOSE="1"
|
||||
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
|
||||
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"
|
||||
|
||||
USER root
|
||||
RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
--mount=type=secret,id=GEOIPUPDATE_LICENSE_KEY \
|
||||
mkdir -p /usr/share/GeoIP && \
|
||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 5: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.2 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.4 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
|
@ -42,4 +42,4 @@ See [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Adoption and Contributions
|
||||
|
||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md).
|
||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.4.0"
|
||||
__version__ = "2025.4.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -54,7 +54,7 @@ def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedCom
|
||||
return component
|
||||
|
||||
|
||||
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs): # noqa: W0613
|
||||
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
|
||||
"""Workaround to set a default response for endpoints.
|
||||
Workaround suggested at
|
||||
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
|
||||
|
@ -164,9 +164,7 @@ class BlueprintEntry:
|
||||
"""Get the blueprint model, with yaml tags resolved if present"""
|
||||
return str(self.tag_resolver(self.model, blueprint))
|
||||
|
||||
def get_permissions(
|
||||
self, blueprint: "Blueprint"
|
||||
) -> Generator[BlueprintEntryPermission, None, None]:
|
||||
def get_permissions(self, blueprint: "Blueprint") -> Generator[BlueprintEntryPermission]:
|
||||
"""Get permissions of this entry, with all yaml tags resolved"""
|
||||
for perm in self.permissions:
|
||||
yield BlueprintEntryPermission(
|
||||
|
@ -5,10 +5,10 @@ from typing import Any
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Value as V
|
||||
from django.http.request import HttpRequest
|
||||
from sentry_sdk import get_current_span
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.lib.sentry import get_http_meta
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
_q_default = Q(default=True)
|
||||
@ -32,13 +32,9 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"""Context Processor that injects brand object into every template"""
|
||||
brand = getattr(request, "brand", DEFAULT_BRAND)
|
||||
tenant = getattr(request, "tenant", Tenant())
|
||||
trace = ""
|
||||
span = get_current_span()
|
||||
if span:
|
||||
trace = span.to_traceparent()
|
||||
return {
|
||||
"brand": brand,
|
||||
"footer_links": tenant.footer_links,
|
||||
"sentry_trace": trace,
|
||||
"html_meta": {**get_http_meta()},
|
||||
"version": get_full_version(),
|
||||
}
|
||||
|
@ -99,18 +99,17 @@ class GroupSerializer(ModelSerializer):
|
||||
if superuser
|
||||
else "authentik_core.disable_group_superuser"
|
||||
)
|
||||
has_perm = user.has_perm(perm)
|
||||
if self.instance and not has_perm:
|
||||
has_perm = user.has_perm(perm, self.instance)
|
||||
if not has_perm:
|
||||
raise ValidationError(
|
||||
_(
|
||||
(
|
||||
"User does not have permission to set "
|
||||
"superuser status to {superuser_status}."
|
||||
).format_map({"superuser_status": superuser})
|
||||
if self.instance or superuser:
|
||||
has_perm = user.has_perm(perm) or user.has_perm(perm, self.instance)
|
||||
if not has_perm:
|
||||
raise ValidationError(
|
||||
_(
|
||||
(
|
||||
"User does not have permission to set "
|
||||
"superuser status to {superuser_status}."
|
||||
).format_map({"superuser_status": superuser})
|
||||
)
|
||||
)
|
||||
)
|
||||
return superuser
|
||||
|
||||
class Meta:
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.management import create_permissions
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from guardian.management import create_anonymous_user
|
||||
|
||||
@ -16,6 +17,10 @@ class Command(BaseCommand):
|
||||
"""Check permissions for all apps"""
|
||||
for tenant in Tenant.objects.filter(ready=True):
|
||||
with tenant:
|
||||
# See https://code.djangoproject.com/ticket/28417
|
||||
# Remove potential lingering old permissions
|
||||
call_command("remove_stale_contenttypes", "--no-input")
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
|
||||
create_permissions(app, verbosity=0)
|
||||
|
@ -31,7 +31,10 @@ class PickleSerializer:
|
||||
|
||||
def loads(self, data):
|
||||
"""Unpickle data to be loaded from redis"""
|
||||
return pickle.loads(data) # nosec
|
||||
try:
|
||||
return pickle.loads(data) # nosec
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _migrate_session(
|
||||
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-14 11:15
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def remove_old_authenticated_session_content_type(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
db_alias = schema_editor.connection.alias
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
|
||||
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0047_delete_oldauthenticatedsession"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=remove_old_authenticated_session_content_type,
|
||||
),
|
||||
]
|
@ -21,7 +21,9 @@
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
{% for key, value in html_meta.items %}
|
||||
<meta name="{{key}}" content="{{ value }}" />
|
||||
{% endfor %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
|
@ -124,6 +124,16 @@ class TestGroupsAPI(APITestCase):
|
||||
{"is_superuser": ["User does not have permission to set superuser status to True."]},
|
||||
)
|
||||
|
||||
def test_superuser_no_perm_no_superuser(self):
|
||||
"""Test creating a group without permission and without superuser flag"""
|
||||
assign_perm("authentik_core.add_group", self.login_user)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-list"),
|
||||
data={"name": generate_id(), "is_superuser": False},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_superuser_update_no_perm(self):
|
||||
"""Test updating a superuser group without permission"""
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
|
@ -132,13 +132,14 @@ class LicenseKey:
|
||||
"""Get a summarized version of all (not expired) licenses"""
|
||||
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
|
||||
for lic in License.objects.all():
|
||||
total.internal_users += lic.internal_users
|
||||
total.external_users += lic.external_users
|
||||
if lic.is_valid:
|
||||
total.internal_users += lic.internal_users
|
||||
total.external_users += lic.external_users
|
||||
total.license_flags.extend(lic.status.license_flags)
|
||||
exp_ts = int(mktime(lic.expiry.timetuple()))
|
||||
if total.exp == 0:
|
||||
total.exp = exp_ts
|
||||
total.exp = max(total.exp, exp_ts)
|
||||
total.license_flags.extend(lic.status.license_flags)
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
|
@ -39,6 +39,10 @@ class License(SerializerModel):
|
||||
internal_users = models.BigIntegerField()
|
||||
external_users = models.BigIntegerField()
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return self.expiry >= now()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.enterprise.api import LicenseSerializer
|
||||
|
@ -8,6 +8,7 @@ from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import (
|
||||
THRESHOLD_READ_ONLY_WEEKS,
|
||||
@ -71,9 +72,9 @@ class TestEnterpriseLicense(TestCase):
|
||||
)
|
||||
def test_valid_multiple(self):
|
||||
"""Check license verification"""
|
||||
lic = License.objects.create(key=generate_id())
|
||||
lic = License.objects.create(key=generate_id(), expiry=expiry_valid)
|
||||
self.assertTrue(lic.status.status().is_valid)
|
||||
lic2 = License.objects.create(key=generate_id())
|
||||
lic2 = License.objects.create(key=generate_id(), expiry=expiry_valid)
|
||||
self.assertTrue(lic2.status.status().is_valid)
|
||||
total = LicenseKey.get_total()
|
||||
self.assertEqual(total.internal_users, 200)
|
||||
@ -232,7 +233,9 @@ class TestEnterpriseLicense(TestCase):
|
||||
)
|
||||
def test_expiry_expired(self):
|
||||
"""Check license verification"""
|
||||
License.objects.create(key=generate_id())
|
||||
User.objects.all().delete()
|
||||
License.objects.all().delete()
|
||||
License.objects.create(key=generate_id(), expiry=expiry_expired)
|
||||
self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED)
|
||||
|
||||
@patch(
|
||||
|
@ -57,7 +57,7 @@ class LogEventSerializer(PassiveSerializer):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_logs(log_default_output=True) -> Generator[list[LogEvent], None, None]:
|
||||
def capture_logs(log_default_output=True) -> Generator[list[LogEvent]]:
|
||||
"""Capture log entries created"""
|
||||
logs = []
|
||||
cap = LogCapture()
|
||||
|
@ -15,6 +15,7 @@
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% include "base/header_js.html" %}
|
||||
<style>
|
||||
html,
|
||||
@ -22,7 +23,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background-image: url("{{ flow.background_url }}");
|
||||
background-image: url("{{ flow_background_url }}");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
@ -5,9 +5,9 @@
|
||||
|
||||
{% block head_before %}
|
||||
{{ block.super }}
|
||||
<link rel="prefetch" href="{{ flow.background_url }}" />
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||
<script>ShadyDOM = { force: true };</script>
|
||||
{% endif %}
|
||||
{% include "base/header_js.html" %}
|
||||
<script>
|
||||
@ -21,7 +21,7 @@ window.authentik.flow = {
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
--ak-flow-background: url("{{ flow_background_url }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
@ -13,7 +13,9 @@ class FlowInterfaceView(InterfaceView):
|
||||
"""Flow interface"""
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
kwargs["flow"] = flow
|
||||
kwargs["flow_background_url"] = flow.background_url(self.request)
|
||||
kwargs["inspector"] = "inspector" in self.request.GET
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
@ -363,6 +363,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
|
||||
if not pool_options:
|
||||
pool_options = True
|
||||
# FIXME: Temporarily force pool to be deactivated.
|
||||
# See https://github.com/goauthentik/authentik/issues/14320
|
||||
pool_options = False
|
||||
|
||||
db = {
|
||||
"default": {
|
||||
|
@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
|
||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||
from redis.exceptions import RedisError, ResponseError
|
||||
from rest_framework.exceptions import APIException
|
||||
from sentry_sdk import HttpTransport
|
||||
from sentry_sdk import HttpTransport, get_current_scope
|
||||
from sentry_sdk import init as sentry_sdk_init
|
||||
from sentry_sdk.api import set_tag
|
||||
from sentry_sdk.integrations.argv import ArgvIntegration
|
||||
@ -27,6 +27,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.socket import SocketIntegration
|
||||
from sentry_sdk.integrations.stdlib import StdlibIntegration
|
||||
from sentry_sdk.integrations.threading import ThreadingIntegration
|
||||
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
|
||||
from structlog.stdlib import get_logger
|
||||
from websockets.exceptions import WebSocketException
|
||||
|
||||
@ -95,6 +96,8 @@ def traces_sampler(sampling_context: dict) -> float:
|
||||
return 0
|
||||
if _type == "websocket":
|
||||
return 0
|
||||
if CONFIG.get_bool("debug"):
|
||||
return 1
|
||||
return float(CONFIG.get("error_reporting.sample_rate", 0.1))
|
||||
|
||||
|
||||
@ -167,3 +170,14 @@ def before_send(event: dict, hint: dict) -> dict | None:
|
||||
if settings.DEBUG:
|
||||
return None
|
||||
return event
|
||||
|
||||
|
||||
def get_http_meta():
|
||||
"""Get sentry-related meta key-values"""
|
||||
scope = get_current_scope()
|
||||
meta = {
|
||||
SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
|
||||
}
|
||||
if bag := scope.get_baggage():
|
||||
meta[BAGGAGE_HEADER_NAME] = bag.serialize()
|
||||
return meta
|
||||
|
@ -59,7 +59,7 @@ class PropertyMappingManager:
|
||||
request: HttpRequest | None,
|
||||
return_mapping: bool = False,
|
||||
**kwargs,
|
||||
) -> Generator[tuple[dict, PropertyMapping], None]:
|
||||
) -> Generator[tuple[dict, PropertyMapping]]:
|
||||
"""Iterate over all mappings that were pre-compiled and
|
||||
execute all of them with the given context"""
|
||||
if not self.__has_compiled:
|
||||
|
@ -494,86 +494,88 @@ class TestConfig(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_db_pool(self):
|
||||
"""Test DB Config with pool"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.host", "foo")
|
||||
config.set("postgresql.name", "foo")
|
||||
config.set("postgresql.user", "foo")
|
||||
config.set("postgresql.password", "foo")
|
||||
config.set("postgresql.port", "foo")
|
||||
config.set("postgresql.test.name", "foo")
|
||||
config.set("postgresql.use_pool", True)
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf,
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"pool": True,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"sslmode": None,
|
||||
"sslrootcert": None,
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
# FIXME: Temporarily force pool to be deactivated.
|
||||
# See https://github.com/goauthentik/authentik/issues/14320
|
||||
# def test_db_pool(self):
|
||||
# """Test DB Config with pool"""
|
||||
# config = ConfigLoader()
|
||||
# config.set("postgresql.host", "foo")
|
||||
# config.set("postgresql.name", "foo")
|
||||
# config.set("postgresql.user", "foo")
|
||||
# config.set("postgresql.password", "foo")
|
||||
# config.set("postgresql.port", "foo")
|
||||
# config.set("postgresql.test.name", "foo")
|
||||
# config.set("postgresql.use_pool", True)
|
||||
# conf = django_db_config(config)
|
||||
# self.assertEqual(
|
||||
# conf,
|
||||
# {
|
||||
# "default": {
|
||||
# "ENGINE": "authentik.root.db",
|
||||
# "HOST": "foo",
|
||||
# "NAME": "foo",
|
||||
# "OPTIONS": {
|
||||
# "pool": True,
|
||||
# "sslcert": None,
|
||||
# "sslkey": None,
|
||||
# "sslmode": None,
|
||||
# "sslrootcert": None,
|
||||
# },
|
||||
# "PASSWORD": "foo",
|
||||
# "PORT": "foo",
|
||||
# "TEST": {"NAME": "foo"},
|
||||
# "USER": "foo",
|
||||
# "CONN_MAX_AGE": 0,
|
||||
# "CONN_HEALTH_CHECKS": False,
|
||||
# "DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
# }
|
||||
# },
|
||||
# )
|
||||
|
||||
def test_db_pool_options(self):
|
||||
"""Test DB Config with pool"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.host", "foo")
|
||||
config.set("postgresql.name", "foo")
|
||||
config.set("postgresql.user", "foo")
|
||||
config.set("postgresql.password", "foo")
|
||||
config.set("postgresql.port", "foo")
|
||||
config.set("postgresql.test.name", "foo")
|
||||
config.set("postgresql.use_pool", True)
|
||||
config.set(
|
||||
"postgresql.pool_options",
|
||||
base64.b64encode(
|
||||
dumps(
|
||||
{
|
||||
"max_size": 15,
|
||||
}
|
||||
).encode()
|
||||
).decode(),
|
||||
)
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf,
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"max_size": 15,
|
||||
},
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"sslmode": None,
|
||||
"sslrootcert": None,
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
# def test_db_pool_options(self):
|
||||
# """Test DB Config with pool"""
|
||||
# config = ConfigLoader()
|
||||
# config.set("postgresql.host", "foo")
|
||||
# config.set("postgresql.name", "foo")
|
||||
# config.set("postgresql.user", "foo")
|
||||
# config.set("postgresql.password", "foo")
|
||||
# config.set("postgresql.port", "foo")
|
||||
# config.set("postgresql.test.name", "foo")
|
||||
# config.set("postgresql.use_pool", True)
|
||||
# config.set(
|
||||
# "postgresql.pool_options",
|
||||
# base64.b64encode(
|
||||
# dumps(
|
||||
# {
|
||||
# "max_size": 15,
|
||||
# }
|
||||
# ).encode()
|
||||
# ).decode(),
|
||||
# )
|
||||
# conf = django_db_config(config)
|
||||
# self.assertEqual(
|
||||
# conf,
|
||||
# {
|
||||
# "default": {
|
||||
# "ENGINE": "authentik.root.db",
|
||||
# "HOST": "foo",
|
||||
# "NAME": "foo",
|
||||
# "OPTIONS": {
|
||||
# "pool": {
|
||||
# "max_size": 15,
|
||||
# },
|
||||
# "sslcert": None,
|
||||
# "sslkey": None,
|
||||
# "sslmode": None,
|
||||
# "sslrootcert": None,
|
||||
# },
|
||||
# "PASSWORD": "foo",
|
||||
# "PORT": "foo",
|
||||
# "TEST": {"NAME": "foo"},
|
||||
# "USER": "foo",
|
||||
# "CONN_MAX_AGE": 0,
|
||||
# "CONN_HEALTH_CHECKS": False,
|
||||
# "DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
# }
|
||||
# },
|
||||
# )
|
||||
|
@ -199,7 +199,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
chunk_size = len(ops)
|
||||
if len(ops) < 1:
|
||||
return
|
||||
for chunk in batched(ops, chunk_size):
|
||||
for chunk in batched(ops, chunk_size, strict=False):
|
||||
req = PatchRequest(Operations=list(chunk))
|
||||
self._request(
|
||||
"PATCH",
|
||||
|
@ -11,7 +11,7 @@ from django.test.runner import DiscoverRunner
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sentry import sentry_init
|
||||
from authentik.root.signals import post_startup, pre_startup, startup
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
from tests.docker import get_docker_tag
|
||||
|
||||
# globally set maxDiff to none to show full assert error
|
||||
TestCase.maxDiff = None
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.4.0 Blueprint schema",
|
||||
"title": "authentik 2025.4.1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -55,7 +55,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
14
go.mod
14
go.mod
@ -5,7 +5,7 @@ go 1.24.0
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/getsentry/sentry-go v0.32.0
|
||||
github.com/getsentry/sentry-go v0.33.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
@ -19,18 +19,18 @@ require (
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/redis/go-redis/v9 v9.8.0
|
||||
github.com/sethvargo/go-envconfig v1.2.0
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025040.1
|
||||
goauthentik.io/api/v3 v3.2025041.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.14.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
@ -75,7 +75,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
32
go.sum
32
go.sum
@ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
|
||||
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
|
||||
github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg=
|
||||
github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@ -230,8 +230,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
|
||||
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@ -251,8 +251,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sethvargo/go-envconfig v1.2.0 h1:q3XkOZWkC+G1sMLCrw9oPGTjYexygLOXDmGUit1ti8Q=
|
||||
github.com/sethvargo/go-envconfig v1.2.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
|
||||
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
|
||||
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025040.1 h1:rQEcMNpz84/LPX8LVFteOJuserrd4PnU4k1Iu/wWqhs=
|
||||
goauthentik.io/api/v3 v3.2025040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025041.1 h1:GAN6AoTmfnCGgx1SyM07jP4/LR/T3rkTEyShSBd3Co8=
|
||||
goauthentik.io/api/v3 v3.2025041.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=
|
||||
@ -358,16 +358,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -376,8 +376,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -412,8 +412,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.4.0"
|
||||
const VERSION = "2025.4.1"
|
||||
|
@ -56,6 +56,7 @@ EXPOSE 3389 6636 9300
|
||||
|
||||
USER 1000
|
||||
|
||||
ENV GOFIPS=1
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/ldap"]
|
||||
|
@ -97,6 +97,7 @@ elif [[ "$1" == "test-all" ]]; then
|
||||
elif [[ "$1" == "healthcheck" ]]; then
|
||||
run_authentik healthcheck $(cat $MODE_FILE)
|
||||
elif [[ "$1" == "dump_config" ]]; then
|
||||
shift
|
||||
exec python -m authentik.lib.config $@
|
||||
elif [[ "$1" == "debug" ]]; then
|
||||
exec sleep infinity
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1013.0",
|
||||
"aws-cdk": "^2.1015.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1013.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1013.0.tgz",
|
||||
"integrity": "sha512-cbq4cOoEIZueMWenGgfI4RujS+AQ9GaMCTlW/3CnvEIhMD8j/tgZx7PTtgMuvwYrRoEeb/wTxgLPgUd5FhsoHA==",
|
||||
"version": "2.1015.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1015.0.tgz",
|
||||
"integrity": "sha512-txd+yMVVybtLfiwT409+fahbP0SkiwhmQvQf6PVVYnWzDPSknxYlUNJHisHV4tJEcbHWn1QPsLmqqMT0bw8hBg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1013.0",
|
||||
"aws-cdk": "^2.1015.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.4.0
|
||||
Default: 2025.4.1
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
Binary file not shown.
BIN
locale/pt/LC_MESSAGES/django.mo
Normal file
BIN
locale/pt/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
3924
locale/pt/LC_MESSAGES/django.po
Normal file
3924
locale/pt/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
538
package-lock.json
generated
538
package-lock.json
generated
@ -1,12 +1,546 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1",
|
||||
"version": "2025.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1"
|
||||
"version": "2025.4.0",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
|
||||
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
|
||||
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.27.0",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/template": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz",
|
||||
"integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
|
||||
"integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.26.5",
|
||||
"@babel/parser": "^7.26.7",
|
||||
"@babel/traverse": "^7.26.7",
|
||||
"@babel/types": "^7.26.7",
|
||||
"javascript-natural-sort": "^0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/compiler-sfc": "3.x",
|
||||
"prettier": "2.x - 3.x",
|
||||
"prettier-plugin-svelte": "3.x",
|
||||
"svelte": "4.x || 5.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/compiler-sfc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detect-indent": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
|
||||
"integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
|
||||
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/get-stdin": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
|
||||
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/git-hooks-list": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.2.0.tgz",
|
||||
"integrity": "sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
|
||||
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-packagejson": {
|
||||
"version": "2.5.10",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz",
|
||||
"integrity": "sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sort-package-json": "2.15.1",
|
||||
"synckit": "0.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": ">= 1.16.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-object-keys": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz",
|
||||
"integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.15.1.tgz",
|
||||
"integrity": "sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-indent": "^7.0.1",
|
||||
"detect-newline": "^4.0.0",
|
||||
"get-stdin": "^9.0.0",
|
||||
"git-hooks-list": "^3.0.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"semver": "^7.6.0",
|
||||
"sort-object-keys": "^1.1.3",
|
||||
"tinyglobby": "^0.2.9"
|
||||
},
|
||||
"bin": {
|
||||
"sort-package-json": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
||||
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
package.json
14
package.json
@ -1,5 +1,15 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.4.0",
|
||||
"private": true
|
||||
"version": "2025.4.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"workspaces": [],
|
||||
"prettier": "./packages/prettier-config/index.js"
|
||||
}
|
||||
|
4
packages/docusaurus-config/package-lock.json
generated
4
packages/docusaurus-config/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/docusaurus-config",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/docusaurus-config",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/docusaurus-config",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "authentik's Docusaurus config",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "@goauthentik/monorepo",
|
||||
"version": "1.0.0",
|
||||
"description": "Utilities for the authentik monorepo.",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": "./index.js",
|
||||
"types": "./out/index.d.ts"
|
||||
}
|
||||
},
|
||||
"types": "./out/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* @typedef {'~authentik'} MonoRepoRoot
|
||||
*/
|
||||
|
||||
/**
|
||||
* The root of the authentik monorepo.
|
||||
*/
|
||||
export const MonoRepoRoot = /** @type {MonoRepoRoot} */ (resolve(__dirname, "..", ".."));
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Resolve a package name to its location in the monorepo to the single node_modules directory.
|
||||
* @param {string} packageName
|
||||
* @returns {string} The resolved path to the package.
|
||||
* @throws {Error} If the package cannot be resolved.
|
||||
*/
|
||||
export function resolvePackage(packageName) {
|
||||
const packageJSONPath = require.resolve(join(packageName, "package.json"), {
|
||||
paths: [MonoRepoRoot],
|
||||
});
|
||||
|
||||
return dirname(packageJSONPath);
|
||||
}
|
@ -76,6 +76,7 @@ EXPOSE 9000 9300 9443
|
||||
|
||||
USER 1000
|
||||
|
||||
ENV GOFIPS=1
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/proxy"]
|
||||
|
200
pyproject.toml
200
pyproject.toml
@ -1,104 +1,116 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.4.0"
|
||||
version = "2025.4.1"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.12.*"
|
||||
requires-python = "==3.13.*"
|
||||
dependencies = [
|
||||
"argon2-cffi",
|
||||
"celery",
|
||||
"channels",
|
||||
"channels-redis",
|
||||
"cryptography",
|
||||
"dacite",
|
||||
"deepmerge",
|
||||
"defusedxml",
|
||||
"django",
|
||||
"django-countries",
|
||||
"django-cte",
|
||||
"django-filter",
|
||||
"django-guardian",
|
||||
"django-model-utils",
|
||||
"django-pglock",
|
||||
"django-prometheus",
|
||||
"django-redis",
|
||||
"django-storages[s3]",
|
||||
"django-tenants",
|
||||
"djangorestframework",
|
||||
"djangorestframework-guardian",
|
||||
"docker",
|
||||
"drf-orjson-renderer",
|
||||
"drf-spectacular",
|
||||
"dumb-init",
|
||||
"duo-client",
|
||||
"fido2",
|
||||
"flower",
|
||||
"geoip2",
|
||||
"geopy",
|
||||
"google-api-python-client",
|
||||
"gssapi",
|
||||
"gunicorn",
|
||||
"jsonpatch",
|
||||
"jwcrypto",
|
||||
"kubernetes",
|
||||
"ldap3",
|
||||
"lxml",
|
||||
"msgraph-sdk",
|
||||
"opencontainers",
|
||||
"packaging",
|
||||
"paramiko",
|
||||
"psycopg[c, pool]",
|
||||
"pydantic",
|
||||
"pydantic-scim",
|
||||
"pyjwt",
|
||||
"pyrad",
|
||||
"python-kadmin-rs ==0.6.0",
|
||||
"pyyaml",
|
||||
"requests-oauthlib",
|
||||
"scim2-filter-parser",
|
||||
"sentry-sdk",
|
||||
"service_identity",
|
||||
"setproctitle",
|
||||
"structlog",
|
||||
"swagger-spec-validator",
|
||||
"tenant-schemas-celery",
|
||||
"twilio",
|
||||
"ua-parser",
|
||||
"unidecode",
|
||||
"urllib3 <3",
|
||||
"uvicorn[standard]",
|
||||
"watchdog",
|
||||
"webauthn",
|
||||
"wsproto",
|
||||
"xmlsec <= 1.3.14",
|
||||
"zxcvbn",
|
||||
"argon2-cffi==23.1.0",
|
||||
"celery==5.5.2",
|
||||
"channels==4.2.2",
|
||||
"channels-redis==4.2.1",
|
||||
"cryptography==44.0.3",
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
"django==5.1.9",
|
||||
"django-countries==7.6.1",
|
||||
"django-cte==1.3.3",
|
||||
"django-filter==25.1",
|
||||
"django-guardian<3.0.0",
|
||||
"django-model-utils==5.0.0",
|
||||
"django-pglock==1.7.2",
|
||||
"django-prometheus==2.3.1",
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-tenants==3.7.0",
|
||||
"djangorestframework==3.16.0",
|
||||
"djangorestframework-guardian==0.3.0",
|
||||
"docker==7.1.0",
|
||||
"drf-orjson-renderer==1.7.3",
|
||||
"drf-spectacular==0.28.0",
|
||||
"dumb-init==1.2.5.post1",
|
||||
"duo-client==5.5.0",
|
||||
"fido2==1.2.0",
|
||||
"flower==2.0.1",
|
||||
"geoip2==5.1.0",
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.169.0",
|
||||
"gssapi==1.9.0",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonpatch==1.33",
|
||||
"jwcrypto==1.5.6",
|
||||
"kubernetes==32.0.1",
|
||||
"ldap3==2.9.1",
|
||||
"lxml==5.4.0",
|
||||
"msgraph-sdk==1.30.0",
|
||||
"opencontainers==0.0.14",
|
||||
"packaging==25.0",
|
||||
"paramiko==3.5.1",
|
||||
"psycopg[c,pool]==3.2.9",
|
||||
"pydantic==2.11.4",
|
||||
"pydantic-scim==0.0.8",
|
||||
"pyjwt==2.10.1",
|
||||
"pyrad==2.4",
|
||||
"python-kadmin-rs==0.6.0",
|
||||
"pyyaml==6.0.2",
|
||||
"requests-oauthlib==2.0.0",
|
||||
"scim2-filter-parser==0.7.0",
|
||||
"sentry-sdk==2.28.0",
|
||||
"service-identity==24.2.0",
|
||||
"setproctitle==1.3.6",
|
||||
"structlog==25.3.0",
|
||||
"swagger-spec-validator==3.0.4",
|
||||
"tenant-schemas-celery==4.0.1",
|
||||
"twilio==9.6.1",
|
||||
"ua-parser==1.0.1",
|
||||
"unidecode==1.4.0",
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.34.2",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.5.2",
|
||||
"wsproto==1.2.0",
|
||||
"xmlsec==1.3.15",
|
||||
"zxcvbn==4.5.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"aws-cdk-lib",
|
||||
"bandit",
|
||||
"black",
|
||||
"bump2version",
|
||||
"channels[daphne]",
|
||||
"codespell",
|
||||
"colorama",
|
||||
"constructs",
|
||||
"coverage[toml]",
|
||||
"debugpy",
|
||||
"drf-jsonschema-serializer",
|
||||
"freezegun",
|
||||
"importlib-metadata",
|
||||
"k5test",
|
||||
"pdoc",
|
||||
"pytest",
|
||||
"pytest-django",
|
||||
"pytest-github-actions-annotate-failures",
|
||||
"pytest-randomly",
|
||||
"pytest-timeout",
|
||||
"requests-mock",
|
||||
"ruff",
|
||||
"selenium",
|
||||
"aws-cdk-lib==2.188.0",
|
||||
"bandit==1.8.3",
|
||||
"black==25.1.0",
|
||||
"bump2version==1.0.1",
|
||||
"channels[daphne]==4.2.2",
|
||||
"codespell==2.4.1",
|
||||
"colorama==0.4.6",
|
||||
"constructs==10.4.2",
|
||||
"coverage[toml]==7.8.0",
|
||||
"debugpy==1.8.14",
|
||||
"drf-jsonschema-serializer==3.0.0",
|
||||
"freezegun==1.5.1",
|
||||
"importlib-metadata==8.6.1",
|
||||
"k5test==0.10.4",
|
||||
"pdoc==15.0.3",
|
||||
"pytest==8.3.5",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest-github-actions-annotate-failures==0.3.0",
|
||||
"pytest-randomly==3.16.0",
|
||||
"pytest-timeout==2.4.0",
|
||||
"requests-mock==1.12.1",
|
||||
"ruff==0.11.9",
|
||||
"selenium==4.32.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
no-binary-package = [
|
||||
# This differs from the no-binary packages in the Dockerfile. This is due to the fact
|
||||
# that these packages are built from source for different reasons than cryptography and kadmin.
|
||||
# These packages are built from source to link against the libxml2 on the system which is
|
||||
# required for functionality and to stay up-to-date on both libraries.
|
||||
# The other packages specified in the dockerfile are compiled from source to link against the
|
||||
# correct FIPS OpenSSL libraries
|
||||
"lxml",
|
||||
"xmlsec",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
@ -143,12 +155,12 @@ ignore-words = ".github/codespell-words.txt"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py312']
|
||||
target-version = ['py313']
|
||||
exclude = 'node_modules'
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
target-version = "py313"
|
||||
exclude = ["**/migrations/**", "**/node_modules/**"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
@ -56,6 +56,7 @@ HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/rac", "healthch
|
||||
|
||||
USER 1000
|
||||
|
||||
ENV GOFIPS=1
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/rac"]
|
||||
|
@ -56,6 +56,7 @@ EXPOSE 1812/udp 9300
|
||||
|
||||
USER 1000
|
||||
|
||||
ENV GOFIPS=1
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/radius"]
|
||||
|
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.4.0
|
||||
version: 2025.4.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
@ -0,0 +1,12 @@
|
||||
import socket
|
||||
from os import environ
|
||||
|
||||
IS_CI = "CI" in environ
|
||||
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
"""Get the local machine's IP"""
|
||||
hostname = socket.gethostname()
|
||||
ip_addr = socket.gethostbyname(hostname)
|
||||
return ip_addr
|
||||
|
190
tests/browser.py
Normal file
190
tests/browser.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
# This file cannot import anything django or anything that will load django
|
||||
|
||||
import json
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.case import TestCase
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from django.urls import reverse
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.remote.command import Command
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from tests import IS_CI, RETRIES, get_local_ip
|
||||
from tests.websocket import BaseWebsocketTestCase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
class BaseSeleniumTestCase(TestCase):
|
||||
"""Mixin which adds helpers for spinning up Selenium"""
|
||||
|
||||
host = get_local_ip()
|
||||
wait_timeout: int
|
||||
user: "User"
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
from django.apps import apps
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
apps.get_app_config("authentik_tenants").ready()
|
||||
self.wait_timeout = 60
|
||||
self.driver = self._get_driver()
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, self.wait_timeout)
|
||||
self.logger = get_logger()
|
||||
self.user = create_test_admin_user()
|
||||
super().setUp()
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
count = 0
|
||||
try:
|
||||
opts = webdriver.ChromeOptions()
|
||||
opts.add_argument("--disable-search-engine-choice-screen")
|
||||
return webdriver.Chrome(options=opts)
|
||||
except WebDriverException:
|
||||
pass
|
||||
while count < RETRIES:
|
||||
try:
|
||||
driver = webdriver.Remote(
|
||||
command_executor="http://localhost:4444/wd/hub",
|
||||
options=webdriver.ChromeOptions(),
|
||||
)
|
||||
driver.maximize_window()
|
||||
return driver
|
||||
except WebDriverException:
|
||||
count += 1
|
||||
raise ValueError(f"Webdriver failed after {RETRIES}.")
|
||||
|
||||
def tearDown(self):
|
||||
if IS_CI:
|
||||
print("::endgroup::", file=stderr)
|
||||
super().tearDown()
|
||||
if IS_CI:
|
||||
print("::group::Browser logs")
|
||||
# Very verbose way to get browser logs
|
||||
# https://github.com/SeleniumHQ/selenium/pull/15641
|
||||
# for some reason this removes the `get_log` API from Remote Webdriver
|
||||
# and only keeps it on the local Chrome web driver, even when using
|
||||
# a remote chrome driver...? (nvm the fact this was released as a minor version)
|
||||
for line in self.driver.execute(Command.GET_LOG, {"type": "browser"})["value"]:
|
||||
print(line["message"])
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
self.driver.quit()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url == desired_url,
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
||||
)
|
||||
|
||||
def url(self, view, query: dict | None = None, **kwargs) -> str:
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
url = self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
if query:
|
||||
return url + "?" + urlencode(query)
|
||||
return url
|
||||
|
||||
def if_user_url(self, path: str | None = None) -> str:
|
||||
"""same as self.url() but show URL in shell"""
|
||||
url = self.url("authentik_core:if-user")
|
||||
if path:
|
||||
return f"{url}#{path}"
|
||||
return url
|
||||
|
||||
def get_shadow_root(
|
||||
self, selector: str, container: WebElement | WebDriver | None = None
|
||||
) -> WebElement:
|
||||
"""Get shadow root element's inner shadowRoot"""
|
||||
if not container:
|
||||
container = self.driver
|
||||
shadow_root = container.find_element(By.CSS_SELECTOR, selector)
|
||||
element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root)
|
||||
return element
|
||||
|
||||
def shady_dom(self) -> WebElement:
|
||||
class wrapper:
|
||||
def __init__(self, container: WebDriver):
|
||||
self.container = container
|
||||
|
||||
def find_element(self, by: str, selector: str) -> WebElement:
|
||||
return self.container.execute_script(
|
||||
"return document.__shady_native_querySelector(arguments[0])", selector
|
||||
)
|
||||
|
||||
return wrapper(self.driver)
|
||||
|
||||
def login(self, shadow_dom=True):
|
||||
"""Do entire login flow"""
|
||||
|
||||
if shadow_dom:
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
else:
|
||||
flow_executor = self.shady_dom()
|
||||
identification_stage = self.shady_dom()
|
||||
|
||||
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=uidField]")))
|
||||
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click()
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
|
||||
Keys.ENTER
|
||||
)
|
||||
|
||||
if shadow_dom:
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
|
||||
else:
|
||||
flow_executor = self.shady_dom()
|
||||
password_stage = self.shady_dom()
|
||||
|
||||
wait = WebDriverWait(password_stage, self.wait_timeout)
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=password]")))
|
||||
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
|
||||
sleep(1)
|
||||
|
||||
def assert_user(self, expected_user: "User"):
|
||||
"""Check users/me API and assert it matches expected_user"""
|
||||
from authentik.core.api.users import UserSerializer
|
||||
|
||||
self.driver.get(self.url("authentik_api:user-me") + "?format=json")
|
||||
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
user = UserSerializer(data=json.loads(user_json)["user"])
|
||||
user.is_valid()
|
||||
self.assertEqual(user["username"].value, expected_user.username)
|
||||
self.assertEqual(user["name"].value, expected_user.name)
|
||||
self.assertEqual(user["email"].value, expected_user.email)
|
||||
|
||||
|
||||
class SeleniumTestCase(BaseSeleniumTestCase, StaticLiveServerTestCase):
|
||||
"""Test case which spins up a selenium instance and a HTTP-only test server"""
|
||||
|
||||
|
||||
class WebsocketSeleniumTestCase(BaseSeleniumTestCase, BaseWebsocketTestCase):
|
||||
"""Test case which spins up a selenium instance and a Websocket/HTTP test server"""
|
48
tests/decorators.py
Normal file
48
tests/decorators.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from tests import RETRIES
|
||||
|
||||
|
||||
def retry(max_retires=RETRIES, exceptions=None):
|
||||
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||
|
||||
if not exceptions:
|
||||
exceptions = [WebDriverException, TimeoutException, NoSuchElementException]
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
def retry_actual(func: Callable):
|
||||
"""Retry test multiple times"""
|
||||
count = 1
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self: TransactionTestCase, *args, **kwargs):
|
||||
"""Run test again if we're below max_retries, including tearDown and
|
||||
setUp. Otherwise raise the error"""
|
||||
nonlocal count
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
except tuple(exceptions) as exc:
|
||||
count += 1
|
||||
if count > max_retires:
|
||||
logger.debug("Exceeded retry count", exc=exc, test=self)
|
||||
|
||||
raise exc
|
||||
logger.debug("Retrying on error", exc=exc, test=self)
|
||||
self.tearDown()
|
||||
self._post_teardown()
|
||||
self._pre_setup()
|
||||
self.setUp()
|
||||
return wrapper(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return retry_actual
|
139
tests/docker.py
Normal file
139
tests/docker.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Docker testing helpers"""
|
||||
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.case import TestCase
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.errors import DockerException
|
||||
from docker.models.containers import Container
|
||||
from docker.models.networks import Network
|
||||
|
||||
from authentik.lib.generators import generate_id
|
||||
from tests import IS_CI
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.models import Outpost
|
||||
|
||||
|
||||
def get_docker_tag() -> str:
|
||||
"""Get docker-tag based off of CI variables"""
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
branch_name = os.environ.get(default_branch, "main")
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
return f"gh-{branch_name}"
|
||||
|
||||
|
||||
class DockerTestCase(TestCase):
|
||||
"""Mixin for dealing with containers"""
|
||||
|
||||
max_healthcheck_attempts = 30
|
||||
|
||||
__client: DockerClient
|
||||
__network: Network
|
||||
|
||||
__label_id = generate_id()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.__client = from_env()
|
||||
self.__network = self.docker_client.networks.create(
|
||||
name=f"authentik-test-{self.__label_id}"
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
@property
|
||||
def docker_client(self) -> DockerClient:
|
||||
return self.__client
|
||||
|
||||
@property
|
||||
def docker_network(self) -> Network:
|
||||
return self.__network
|
||||
|
||||
@property
|
||||
def docker_labels(self) -> dict:
|
||||
return {"io.goauthentik.test": self.__label_id}
|
||||
|
||||
def get_container_image(self, base: str) -> str:
|
||||
"""Try to pull docker image based on git branch, fallback to main if not found."""
|
||||
image = f"{base}:gh-main"
|
||||
if not IS_CI:
|
||||
return image
|
||||
try:
|
||||
branch_image = f"{base}:{get_docker_tag()}"
|
||||
self.docker_client.images.pull(branch_image)
|
||||
return branch_image
|
||||
except DockerException:
|
||||
self.docker_client.images.pull(image)
|
||||
return image
|
||||
|
||||
def run_container(self, **specs: dict[str, Any]) -> Container:
|
||||
if "network_mode" not in specs:
|
||||
specs["network"] = self.__network.name
|
||||
specs["labels"] = self.docker_labels
|
||||
specs["detach"] = True
|
||||
if hasattr(self, "live_server_url"):
|
||||
specs.setdefault("environment", {})
|
||||
specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url
|
||||
container = self.docker_client.containers.run(**specs)
|
||||
container.reload()
|
||||
state = container.attrs.get("State", {})
|
||||
if "Health" not in state:
|
||||
return container
|
||||
self.wait_for_container(container)
|
||||
return container
|
||||
|
||||
def output_container_logs(self, container: Container | None = None):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
if IS_CI:
|
||||
image = container.image
|
||||
tags = image.tags[0] if len(image.tags) > 0 else str(image)
|
||||
print(f"::group::Container logs - {tags}")
|
||||
for log in container.logs().decode().split("\n"):
|
||||
print(log)
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
|
||||
def tearDown(self):
|
||||
containers: list[Container] = self.docker_client.containers.list(
|
||||
filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())}
|
||||
)
|
||||
for container in containers:
|
||||
self.output_container_logs(container)
|
||||
try:
|
||||
container.stop()
|
||||
except DockerException:
|
||||
pass
|
||||
try:
|
||||
container.remove(force=True)
|
||||
except DockerException:
|
||||
pass
|
||||
self.__network.remove()
|
||||
super().tearDown()
|
||||
|
||||
def wait_for_container(self, container: Container):
|
||||
"""Check that container is health"""
|
||||
attempt = 0
|
||||
while attempt < self.max_healthcheck_attempts:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
attempt += 1
|
||||
sleep(0.5)
|
||||
self.failureException("Container failed to start")
|
||||
|
||||
def wait_for_outpost(self, outpost: "Outpost"):
|
||||
# Wait until outpost healthcheck succeeds
|
||||
attempt = 0
|
||||
while attempt < self.max_healthcheck_attempts:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
return
|
||||
attempt += 1
|
||||
sleep(0.5)
|
||||
self.failureException("Outpost failed to become healthy")
|
@ -1,12 +1,12 @@
|
||||
services:
|
||||
chrome:
|
||||
image: docker.io/selenium/standalone-chrome:122.0
|
||||
image: docker.io/selenium/standalone-chrome:136.0
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.6.5
|
||||
image: docker.io/axllent/mailpit:v1.24.2
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
@ -18,10 +18,12 @@ from authentik.stages.authenticator_static.models import (
|
||||
StaticToken,
|
||||
)
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDevice
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||
class TestFlowsAuthenticator(DockerTestCase, SeleniumTestCase):
|
||||
"""test flow with otp stages"""
|
||||
|
||||
@retry()
|
||||
|
@ -11,10 +11,12 @@ from authentik.core.models import User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
class TestFlowsEnroll(DockerTestCase, SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
@retry()
|
||||
|
@ -1,12 +1,21 @@
|
||||
"""test default login flow"""
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from authentik.flows.models import Flow
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
class TestFlowsLogin(DockerTestCase, SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def tearDown(self):
|
||||
# Reset authentication flow's compatibility mode; we need to do this as its
|
||||
# not specified in the blueprint
|
||||
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=False)
|
||||
return super().tearDown()
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@ -23,3 +32,21 @@ class TestFlowsLogin(SeleniumTestCase):
|
||||
self.login()
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.assert_user(self.user)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
def test_login_compatibility_mode(self):
|
||||
"""test default login flow with compatibility mode enabled"""
|
||||
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=True)
|
||||
self.driver.get(
|
||||
self.url(
|
||||
"authentik_core:if-flow",
|
||||
flow_slug="default-authentication-flow",
|
||||
)
|
||||
)
|
||||
self.login(shadow_dom=False)
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.assert_user(self.user)
|
||||
|
53
tests/e2e/test_flows_login_sfe.py
Normal file
53
tests/e2e/test_flows_login_sfe.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""test default login (using SFE interface) flow"""
|
||||
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsLoginSFE(DockerTestCase, SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def login(self):
|
||||
"""Do entire login flow adjusted for SFE"""
|
||||
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
|
||||
identification_stage = flow_executor.find_element(By.ID, "ident-form")
|
||||
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
|
||||
Keys.ENTER
|
||||
)
|
||||
|
||||
password_stage = flow_executor.find_element(By.ID, "password-form")
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
|
||||
sleep(1)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
def test_login(self):
|
||||
"""test default login flow"""
|
||||
self.driver.get(
|
||||
self.url(
|
||||
"authentik_core:if-flow",
|
||||
flow_slug="default-authentication-flow",
|
||||
query={"sfe": True},
|
||||
)
|
||||
)
|
||||
self.login()
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.assert_user(self.user)
|
@ -13,10 +13,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsRecovery(SeleniumTestCase):
|
||||
class TestFlowsRecovery(DockerTestCase, SeleniumTestCase):
|
||||
"""Test Recovery flow"""
|
||||
|
||||
def initial_stages(self, user: User):
|
||||
|
@ -8,10 +8,12 @@ from authentik.core.models import User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
class TestFlowsStageSetup(DockerTestCase, SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
@retry()
|
||||
|
@ -16,10 +16,12 @@ from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderLDAP(SeleniumTestCase):
|
||||
class TestProviderLDAP(DockerTestCase, WebsocketTestCase):
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
def start_ldap(self, outpost: Outpost):
|
||||
|
@ -18,10 +18,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
class TestProviderOAuth2Github(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
class TestProviderOAuth2OAuth(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
class TestProviderOAuth2OIDC(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
class TestProviderOAuth2OIDCImplicit(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -3,11 +3,8 @@
|
||||
from base64 import b64encode
|
||||
from dataclasses import asdict
|
||||
from json import loads
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skip, skipUnless
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from jwt import decode
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
@ -18,10 +15,13 @@ from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderProxy(SeleniumTestCase):
|
||||
class TestProviderProxy(DockerTestCase, SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -37,13 +37,41 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
"""Start proxy container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
|
||||
ports={
|
||||
"9000": "9000",
|
||||
},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
ports={"9000": "9000"},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
def _prepare(self):
|
||||
# set additionalHeaders to test later
|
||||
self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"}
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
basic_auth_enabled=True,
|
||||
basic_auth_user_attribute="basic-username",
|
||||
basic_auth_password_attribute="basic-password", # nosec
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(outpost)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@ -61,44 +89,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_proxy_simple(self):
|
||||
"""Test simple outpost setup with single provider"""
|
||||
# set additionalHeaders to test later
|
||||
self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"}
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50: # noqa: PLR2004
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
sleep(5)
|
||||
|
||||
self._prepare()
|
||||
self.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
sleep(1)
|
||||
@ -137,49 +128,13 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_proxy_basic_auth(self):
|
||||
"""Test simple outpost setup with single provider"""
|
||||
self._prepare()
|
||||
# Setup basic auth
|
||||
cred = generate_id()
|
||||
attr = "basic-password" # nosec
|
||||
self.user.attributes["basic-username"] = cred
|
||||
self.user.attributes[attr] = cred
|
||||
self.user.attributes["basic-password"] = cred
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
basic_auth_enabled=True,
|
||||
basic_auth_user_attribute="basic-username",
|
||||
basic_auth_password_attribute=attr,
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50: # noqa: PLR2004
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
sleep(5)
|
||||
|
||||
self.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
sleep(1)
|
||||
@ -187,9 +142,9 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(body.get("headers").get("X-Authentik-Username"), [self.user.username])
|
||||
auth_header = b64encode(f"{cred}:{cred}".encode()).decode()
|
||||
self.assertEqual(body["headers"]["Authorization"], [f"Basic {auth_header}"])
|
||||
self.assertEqual(body.get("headers").get("Authorization"), [f"Basic {auth_header}"])
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
@ -199,10 +154,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
|
||||
# TODO: Fix flaky test
|
||||
@skip("Flaky test")
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
class TestProviderProxyConnect(DockerTestCase, WebsocketTestCase):
|
||||
"""Test Proxy connectivity over websockets"""
|
||||
|
||||
@retry(exceptions=[AssertionError])
|
||||
@ -241,14 +193,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50: # noqa: PLR2004
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen and state.version:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
state = outpost.state
|
||||
self.assertGreaterEqual(len(state), 1)
|
||||
|
@ -13,10 +13,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderProxyForward(SeleniumTestCase):
|
||||
class TestProviderProxyForward(DockerTestCase, SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -30,14 +32,11 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
"""Start proxy container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
|
||||
ports={
|
||||
"9000": "9000",
|
||||
},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
ports={"9000": "9000"},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
name="ak-test-outpost",
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@ -77,17 +76,6 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
|
||||
self.start_outpost(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50: # noqa: PLR2004
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
sleep(5)
|
||||
|
||||
@retry()
|
||||
def test_traefik(self):
|
||||
"""Test traefik"""
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Radius e2e tests"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
|
||||
from pyrad.client import Client
|
||||
from pyrad.dictionary import Dictionary
|
||||
@ -9,14 +8,17 @@ from pyrad.packet import AccessAccept, AccessReject, AccessRequest
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||
from authentik.providers.radius.models import RadiusProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderRadius(SeleniumTestCase):
|
||||
class TestProviderRadius(DockerTestCase, WebsocketTestCase):
|
||||
"""Radius Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -28,13 +30,13 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-radius"),
|
||||
ports={"1812/udp": "1812/udp"},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
def _prepare(self) -> User:
|
||||
"""prepare user, provider, app and container"""
|
||||
self.user = create_test_user()
|
||||
radius: RadiusProvider = RadiusProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
@ -50,17 +52,6 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
outpost.providers.add(radius)
|
||||
|
||||
self.start_radius(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50: # noqa: PLR2004
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
sleep(5)
|
||||
return outpost
|
||||
|
||||
@retry()
|
||||
|
@ -14,10 +14,12 @@ from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.sources.saml.processors.constants import SAML_BINDING_POST
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
class TestProviderSAML(DockerTestCase, SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
def setup_client(self, provider: SAMLProvider, force_post: bool = False):
|
||||
|
@ -11,10 +11,12 @@ from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
|
||||
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
|
||||
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestSourceLDAPSamba(SeleniumTestCase):
|
||||
class TestSourceLDAPSamba(DockerTestCase, SeleniumTestCase):
|
||||
"""test LDAP Source"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -16,7 +16,9 @@ from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class OAuth1Callback(OAuthCallback):
|
||||
@ -48,7 +50,7 @@ class OAUth1Type(SourceType):
|
||||
}
|
||||
|
||||
|
||||
class TestSourceOAuth1(SeleniumTestCase):
|
||||
class TestSourceOAuth1(DockerTestCase, SeleniumTestCase):
|
||||
"""Test OAuth1 Source"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
|
@ -16,10 +16,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestSourceOAuth2(SeleniumTestCase):
|
||||
class TestSourceOAuth2(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -16,7 +16,9 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
@ -70,7 +72,7 @@ Sm75WXsflOxuTn08LbgGc4s=
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
class TestSourceSAML(DockerTestCase, SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -8,12 +8,14 @@ from docker.types import Healthcheck
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
TEST_POLL_MAX = 25
|
||||
|
||||
|
||||
class TestSourceSCIM(SeleniumTestCase):
|
||||
class TestSourceSCIM(DockerTestCase, SeleniumTestCase):
|
||||
"""test SCIM Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -1,311 +0,0 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache, wraps
|
||||
from os import environ
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
from unittest.case import TestCase
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from django.db import connection
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from django.urls import reverse
|
||||
from docker import DockerClient, from_env
|
||||
from docker.errors import DockerException
|
||||
from docker.models.containers import Container
|
||||
from docker.models.networks import Network
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
RETRIES = int(environ.get("RETRIES", "3"))
|
||||
IS_CI = "CI" in environ
|
||||
|
||||
|
||||
def get_docker_tag() -> str:
|
||||
"""Get docker-tag based off of CI variables"""
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
branch_name = os.environ.get(default_branch, "main")
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
return f"gh-{branch_name}"
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
"""Get the local machine's IP"""
|
||||
hostname = socket.gethostname()
|
||||
ip_addr = socket.gethostbyname(hostname)
|
||||
return ip_addr
|
||||
|
||||
|
||||
class DockerTestCase(TestCase):
|
||||
"""Mixin for dealing with containers"""
|
||||
|
||||
max_healthcheck_attempts = 30
|
||||
|
||||
__client: DockerClient
|
||||
__network: Network
|
||||
|
||||
__label_id = generate_id()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.__client = from_env()
|
||||
self.__network = self.docker_client.networks.create(name=f"authentik-test-{generate_id()}")
|
||||
|
||||
@property
|
||||
def docker_client(self) -> DockerClient:
|
||||
return self.__client
|
||||
|
||||
@property
|
||||
def docker_network(self) -> Network:
|
||||
return self.__network
|
||||
|
||||
@property
|
||||
def docker_labels(self) -> dict:
|
||||
return {"io.goauthentik.test": self.__label_id}
|
||||
|
||||
def wait_for_container(self, container: Container):
|
||||
"""Check that container is health"""
|
||||
attempt = 0
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
attempt += 1
|
||||
if attempt >= self.max_healthcheck_attempts:
|
||||
self.failureException("Container failed to start")
|
||||
|
||||
def get_container_image(self, base: str) -> str:
|
||||
"""Try to pull docker image based on git branch, fallback to main if not found."""
|
||||
image = f"{base}:gh-main"
|
||||
try:
|
||||
branch_image = f"{base}:{get_docker_tag()}"
|
||||
self.docker_client.images.pull(branch_image)
|
||||
return branch_image
|
||||
except DockerException:
|
||||
self.docker_client.images.pull(image)
|
||||
return image
|
||||
|
||||
def run_container(self, **specs: dict[str, Any]) -> Container:
|
||||
if "network_mode" not in specs:
|
||||
specs["network"] = self.__network.name
|
||||
specs["labels"] = self.docker_labels
|
||||
specs["detach"] = True
|
||||
if hasattr(self, "live_server_url"):
|
||||
specs.setdefault("environment", {})
|
||||
specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url
|
||||
container = self.docker_client.containers.run(**specs)
|
||||
container.reload()
|
||||
state = container.attrs.get("State", {})
|
||||
if "Health" not in state:
|
||||
return container
|
||||
self.wait_for_container(container)
|
||||
return container
|
||||
|
||||
def output_container_logs(self, container: Container | None = None):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
if IS_CI:
|
||||
image = container.image
|
||||
tags = image.tags[0] if len(image.tags) > 0 else str(image)
|
||||
print(f"::group::Container logs - {tags}")
|
||||
for log in container.logs().decode().split("\n"):
|
||||
print(log)
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
|
||||
def tearDown(self):
|
||||
containers: list[Container] = self.docker_client.containers.list(
|
||||
filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())}
|
||||
)
|
||||
for container in containers:
|
||||
self.output_container_logs(container)
|
||||
try:
|
||||
container.kill()
|
||||
except DockerException:
|
||||
pass
|
||||
try:
|
||||
container.remove(force=True)
|
||||
except DockerException:
|
||||
pass
|
||||
self.__network.remove()
|
||||
|
||||
|
||||
class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
|
||||
host = get_local_ip()
|
||||
wait_timeout: int
|
||||
user: User
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
apps.get_app_config("authentik_tenants").ready()
|
||||
self.wait_timeout = 60
|
||||
self.driver = self._get_driver()
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, self.wait_timeout)
|
||||
self.logger = get_logger()
|
||||
self.user = create_test_admin_user()
|
||||
super().setUp()
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
count = 0
|
||||
try:
|
||||
opts = webdriver.ChromeOptions()
|
||||
opts.add_argument("--disable-search-engine-choice-screen")
|
||||
return webdriver.Chrome(options=opts)
|
||||
except WebDriverException:
|
||||
pass
|
||||
while count < RETRIES:
|
||||
try:
|
||||
driver = webdriver.Remote(
|
||||
command_executor="http://localhost:4444/wd/hub",
|
||||
options=webdriver.ChromeOptions(),
|
||||
)
|
||||
driver.maximize_window()
|
||||
return driver
|
||||
except WebDriverException:
|
||||
count += 1
|
||||
raise ValueError(f"Webdriver failed after {RETRIES}.")
|
||||
|
||||
def tearDown(self):
|
||||
if IS_CI:
|
||||
print("::endgroup::", file=stderr)
|
||||
super().tearDown()
|
||||
if IS_CI:
|
||||
print("::group::Browser logs")
|
||||
for line in self.driver.get_log("browser"):
|
||||
print(line["message"])
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
self.driver.quit()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url == desired_url,
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
||||
)
|
||||
|
||||
def url(self, view, query: dict | None = None, **kwargs) -> str:
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
url = self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
if query:
|
||||
return url + "?" + urlencode(query)
|
||||
return url
|
||||
|
||||
def if_user_url(self, path: str | None = None) -> str:
|
||||
"""same as self.url() but show URL in shell"""
|
||||
url = self.url("authentik_core:if-user")
|
||||
if path:
|
||||
return f"{url}#{path}"
|
||||
return url
|
||||
|
||||
def get_shadow_root(
|
||||
self, selector: str, container: WebElement | WebDriver | None = None
|
||||
) -> WebElement:
|
||||
"""Get shadow root element's inner shadowRoot"""
|
||||
if not container:
|
||||
container = self.driver
|
||||
shadow_root = container.find_element(By.CSS_SELECTOR, selector)
|
||||
element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root)
|
||||
return element
|
||||
|
||||
def login(self):
|
||||
"""Do entire login flow and check user afterwards"""
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click()
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
|
||||
Keys.ENTER
|
||||
)
|
||||
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
|
||||
self.user.username
|
||||
)
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
|
||||
sleep(1)
|
||||
|
||||
def assert_user(self, expected_user: User):
|
||||
"""Check users/me API and assert it matches expected_user"""
|
||||
self.driver.get(self.url("authentik_api:user-me") + "?format=json")
|
||||
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
user = UserSerializer(data=json.loads(user_json)["user"])
|
||||
user.is_valid()
|
||||
self.assertEqual(user["username"].value, expected_user.username)
|
||||
self.assertEqual(user["name"].value, expected_user.name)
|
||||
self.assertEqual(user["email"].value, expected_user.email)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_loader():
|
||||
"""Thin wrapper to lazily get a Migration Loader, only when it's needed
|
||||
and only once"""
|
||||
return MigrationLoader(connection)
|
||||
|
||||
|
||||
def retry(max_retires=RETRIES, exceptions=None):
|
||||
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||
|
||||
if not exceptions:
|
||||
exceptions = [WebDriverException, TimeoutException, NoSuchElementException]
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
def retry_actual(func: Callable):
|
||||
"""Retry test multiple times"""
|
||||
count = 1
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self: TransactionTestCase, *args, **kwargs):
|
||||
"""Run test again if we're below max_retries, including tearDown and
|
||||
setUp. Otherwise raise the error"""
|
||||
nonlocal count
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
except tuple(exceptions) as exc:
|
||||
count += 1
|
||||
if count > max_retires:
|
||||
logger.debug("Exceeded retry count", exc=exc, test=self)
|
||||
|
||||
raise exc
|
||||
logger.debug("Retrying on error", exc=exc, test=self)
|
||||
self.tearDown()
|
||||
self._post_teardown()
|
||||
self._pre_setup()
|
||||
self.setUp()
|
||||
return wrapper(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return retry_actual
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -19,7 +19,7 @@ from authentik.outposts.models import (
|
||||
)
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
from tests.docker import DockerTestCase, get_docker_tag
|
||||
|
||||
|
||||
class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
@ -19,7 +19,7 @@ from authentik.outposts.models import (
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.controllers.docker import DockerController
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
from tests.docker import DockerTestCase, get_docker_tag
|
||||
|
||||
|
||||
class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
52
tests/websocket.py
Normal file
52
tests/websocket.py
Normal file
@ -0,0 +1,52 @@
|
||||
# This file cannot import anything django or anything that will load django
|
||||
from sys import stderr
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from daphne.testing import DaphneProcess
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from tests import IS_CI, get_local_ip
|
||||
|
||||
|
||||
def set_database_connection():
|
||||
from django.conf import settings
|
||||
|
||||
settings.DATABASES["default"]["NAME"] = settings.DATABASES["default"]["TEST"]["NAME"]
|
||||
settings.TEST = True
|
||||
|
||||
|
||||
class DatabasePatchDaphneProcess(DaphneProcess):
|
||||
# See https://github.com/django/channels/issues/2048
|
||||
# See https://github.com/django/channels/pull/2033
|
||||
|
||||
def __init__(self, host, get_application, kwargs=None, setup=None, teardown=None):
|
||||
super().__init__(host, get_application, kwargs, setup, teardown)
|
||||
self.setup = set_database_connection
|
||||
|
||||
|
||||
class BaseWebsocketTestCase(ChannelsLiveServerTestCase):
|
||||
"""Base channels test case"""
|
||||
|
||||
host = get_local_ip()
|
||||
ProtocolServerProcess = DatabasePatchDaphneProcess
|
||||
|
||||
|
||||
class WebsocketTestCase(BaseWebsocketTestCase):
|
||||
"""Test case to allow testing against a running Websocket/HTTP server"""
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
from django.apps import apps
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
apps.get_app_config("authentik_tenants").ready()
|
||||
self.logger = get_logger()
|
||||
self.user = create_test_admin_user()
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
if IS_CI:
|
||||
print("::endgroup::", file=stderr)
|
||||
super().tearDown()
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
// TypeScript Project Configuration
|
||||
{
|
||||
"extends": "./packages/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"watchOptions": {
|
||||
"excludeDirectories": [
|
||||
"**/.git", // Git
|
||||
"**/.yarn", // Yarn
|
||||
"**/.vscode", // VS Code
|
||||
"**/.vscode-test-web", // VS Code Web Test
|
||||
"**/dist", // Distributed build files
|
||||
"**/out", // Output build files
|
||||
"**/.drafts", // Drafts
|
||||
"**/.github", // GitHub
|
||||
"**/node_modules" // Node modules
|
||||
]
|
||||
},
|
||||
|
||||
// The root project has no sources of its own. By setting `files` to an empty
|
||||
// list, TS won't automatically include all sources below root (the default).
|
||||
"files": [],
|
||||
"references": [
|
||||
// Note that references are in the order we want them to be built.
|
||||
// TODO: Left blank until TypeScript workspaces are complete.
|
||||
]
|
||||
}
|
@ -2,15 +2,11 @@
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
out
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
# Import order matters
|
||||
poly.ts
|
||||
src/locale-codes.ts
|
||||
src/locales/
|
||||
storybook-static/
|
||||
# Prettier breaks the tsconfig file
|
||||
tsconfig.json
|
||||
.storybook/css-import-maps*
|
||||
package.json
|
||||
packages/**/package.json
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { create } from "@storybook/theming/create";
|
||||
|
||||
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
export default create({
|
||||
base: isDarkMode ? "dark" : "light",
|
||||
brandTitle: "authentik Storybook",
|
||||
brandUrl: "https://goauthentik.io",
|
||||
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
|
||||
brandTarget: "_self",
|
||||
});
|
69
web/.storybook/main.js
Normal file
69
web/.storybook/main.js
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file Storybook configuration.
|
||||
* @import { StorybookConfig } from "@storybook/web-components-vite";
|
||||
* @import { InlineConfig, Plugin } from "vite";
|
||||
*/
|
||||
import { cwd } from "process";
|
||||
import postcssLit from "rollup-plugin-postcss-lit";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
const CSSImportPattern = /import [\w\$]+ from .+\.(css)/g;
|
||||
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
||||
|
||||
/**
|
||||
* @satisfies {Plugin<never>}
|
||||
*/
|
||||
const inlineCSSPlugin = {
|
||||
name: "inline-css-plugin",
|
||||
transform: (source, id) => {
|
||||
if (!JavaScriptFilePattern.test(id)) return;
|
||||
|
||||
const code = source.replace(CSSImportPattern, (match) => {
|
||||
return `${match}?inline`;
|
||||
});
|
||||
|
||||
return {
|
||||
code,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @satisfies {StorybookConfig}
|
||||
*/
|
||||
const config = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-controls",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-addon-mock",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/web-components-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
viteFinal({ plugins = [], ...config }) {
|
||||
/**
|
||||
* @satisfies {InlineConfig}
|
||||
*/
|
||||
const mergedConfig = {
|
||||
...config,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(NODE_ENV),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(process.env.AK_API_BASE_PATH || ""),
|
||||
},
|
||||
plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
|
||||
};
|
||||
|
||||
return mergedConfig;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,81 +0,0 @@
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import type { StorybookConfig } from "@storybook/web-components-vite";
|
||||
import { cwd } from "process";
|
||||
import modify from "rollup-plugin-modify";
|
||||
import postcssLit from "rollup-plugin-postcss-lit";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export const isProdBuild = process.env.NODE_ENV === "production";
|
||||
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
|
||||
|
||||
const importInlinePatterns = [
|
||||
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
|
||||
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
|
||||
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
|
||||
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
|
||||
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
|
||||
'import styles from "\\./LibraryPageImpl\\.css',
|
||||
];
|
||||
|
||||
const importInlineRegexp = new RegExp(importInlinePatterns.map((a) => `(${a})`).join("|"));
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-controls",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-addon-mock",
|
||||
],
|
||||
staticDirs: [
|
||||
{
|
||||
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
|
||||
to: "@patternfly/patternfly/patternfly-base.css",
|
||||
},
|
||||
{
|
||||
from: "../src/common/styles/authentik.css",
|
||||
to: "@goauthentik/common/styles/authentik.css",
|
||||
},
|
||||
{
|
||||
from: "../src/common/styles/theme-dark.css",
|
||||
to: "@goauthentik/common/styles/theme-dark.css",
|
||||
},
|
||||
{
|
||||
from: "../src/common/styles/one-dark.css",
|
||||
to: "@goauthentik/common/styles/one-dark.css",
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/web-components-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
async viteFinal(config) {
|
||||
return {
|
||||
...config,
|
||||
plugins: [
|
||||
modify({
|
||||
find: importInlineRegexp,
|
||||
replace: (match: RegExpMatchArray) => {
|
||||
return `${match}?inline`;
|
||||
},
|
||||
}),
|
||||
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,
|
||||
postcssLit(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
38
web/.storybook/manager.js
Normal file
38
web/.storybook/manager.js
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file Storybook manager configuration.
|
||||
*
|
||||
* @import { ThemeVarsPartial } from "storybook/internal/theming";
|
||||
*/
|
||||
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
||||
import { addons } from "@storybook/manager-api";
|
||||
import { create } from "@storybook/theming/create";
|
||||
|
||||
/**
|
||||
* @satisfies {Partial<ThemeVarsPartial>}
|
||||
*/
|
||||
const baseTheme = {
|
||||
brandTitle: "authentik Storybook",
|
||||
brandUrl: "https://goauthentik.io",
|
||||
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
|
||||
brandTarget: "_self",
|
||||
};
|
||||
|
||||
const uiTheme = resolveUITheme();
|
||||
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
...baseTheme,
|
||||
base: uiTheme,
|
||||
}),
|
||||
enableShortcuts: false,
|
||||
});
|
||||
|
||||
createUIThemeEffect((nextUITheme) => {
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
...baseTheme,
|
||||
base: nextUITheme,
|
||||
}),
|
||||
enableShortcuts: false,
|
||||
});
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
// .storybook/manager.js
|
||||
import { addons } from "@storybook/manager-api";
|
||||
|
||||
import authentikTheme from "./authentikTheme";
|
||||
|
||||
addons.setConfig({
|
||||
theme: authentikTheme,
|
||||
enableShortcuts: false,
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
|
||||
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
|
||||
<style>
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
|
32
web/.storybook/preview.js
Normal file
32
web/.storybook/preview.js
Normal file
@ -0,0 +1,32 @@
|
||||
/// <reference types="../types/css.js" />
|
||||
/**
|
||||
* @file Storybook manager configuration.
|
||||
*
|
||||
* @import { Preview } from "@storybook/web-components";
|
||||
*/
|
||||
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
applyDocumentTheme();
|
||||
|
||||
/**
|
||||
* @satisfies {Preview}
|
||||
*/
|
||||
const preview = {
|
||||
parameters: {
|
||||
options: {
|
||||
storySort: {
|
||||
method: "alphabetical",
|
||||
},
|
||||
},
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
@ -1,30 +0,0 @@
|
||||
import type { Preview } from "@storybook/web-components";
|
||||
|
||||
import "@goauthentik/common/styles/authentik.css";
|
||||
// import "@goauthentik/common/styles/theme-dark.css";
|
||||
import "@patternfly/patternfly/components/Brand/brand.css";
|
||||
import "@patternfly/patternfly/components/Page/page.css";
|
||||
// .storybook/preview.js
|
||||
import "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
options: {
|
||||
storySort: {
|
||||
method: "alphabetical",
|
||||
},
|
||||
},
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
cssUserPrefs: {
|
||||
"prefers-color-scheme": "light",
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user