mirror of
https://github.com/goauthentik/authentik
synced 2026-04-26 01:25:02 +02:00
Compare commits
3 Commits
manualdeps
...
admin/vers
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbce9611d2 | ||
|
|
e6643a69cd | ||
|
|
0fdeaee559 |
@@ -42,7 +42,11 @@ class Exporter:
|
||||
if model in self.excluded_models:
|
||||
continue
|
||||
for obj in self.get_model_instances(model):
|
||||
yield BlueprintEntry.from_model(obj)
|
||||
yield BlueprintEntry.from_model(self.alter_model(obj))
|
||||
|
||||
def alter_model(self, model: Model):
|
||||
"""Hook to modify the model before exporting"""
|
||||
return model
|
||||
|
||||
def get_model_instances(self, model: type[Model]) -> QuerySet:
|
||||
"""Return a queryset for `model`. Can be used to filter some
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -10,17 +11,21 @@ from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.enterprise.bundle import generate_support_bundle
|
||||
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.permissions import HasPermission
|
||||
from authentik.tenants.utils import get_unique_identifier
|
||||
|
||||
|
||||
@@ -147,3 +152,24 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
response.is_valid(raise_exception=True)
|
||||
return Response(response.data)
|
||||
|
||||
|
||||
class BinaryRenderer(BaseRenderer):
|
||||
media_type = "application/gzip"
|
||||
format = "bin"
|
||||
|
||||
|
||||
class SupportBundleView(APIView):
|
||||
"""Generate a support bundle."""
|
||||
|
||||
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
renderer_classes = [BinaryRenderer]
|
||||
|
||||
@extend_schema(responses=bytes, request=None)
|
||||
def post(self, request: Request) -> Response:
|
||||
"""Generate a support bundle."""
|
||||
response = HttpResponse(generate_support_bundle(), content_type=BinaryRenderer.media_type)
|
||||
response["Content-Disposition"] = 'attachment; filename="authentik_support.tgz"'
|
||||
return response
|
||||
|
||||
53
authentik/enterprise/bundle.py
Normal file
53
authentik/enterprise/bundle.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import re
|
||||
from io import BytesIO
|
||||
from tarfile import TarInfo, open
|
||||
|
||||
from django.db.models import Model
|
||||
from django.db.models.fields import CharField, SlugField, TextField
|
||||
from django.db.models.fields.json import JSONField
|
||||
|
||||
from authentik.blueprints.v1.exporter import Exporter
|
||||
from authentik.core.models import User
|
||||
from lifecycle.support import encrypt, generate
|
||||
|
||||
SENSITIVE_VALUE_PLACEHOLDER = "<REDACTED>"
|
||||
|
||||
|
||||
class SupportExporter(Exporter):
|
||||
"""Blueprint exporter which censors sensitive model attributes"""
|
||||
|
||||
sensitive_fields = re.compile(
|
||||
# Partially taken from Django's SafeExceptionReporterFilter
|
||||
"API|AUTH|TOKEN|KEY|SECRET|PASS|SIGNATURE|CREDENTIALS",
|
||||
re.I,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.excluded_models.append(User)
|
||||
|
||||
def alter_model(self, model: Model):
|
||||
for field in model._meta.fields:
|
||||
if not self.sensitive_fields.search(field.name):
|
||||
continue
|
||||
if isinstance(field, TextField | CharField | SlugField):
|
||||
setattr(model, field.name, SENSITIVE_VALUE_PLACEHOLDER)
|
||||
elif isinstance(field, JSONField):
|
||||
setattr(model, field.name, {})
|
||||
return model
|
||||
|
||||
|
||||
def generate_support_bundle():
|
||||
fh = BytesIO()
|
||||
exporter = SupportExporter()
|
||||
files = {
|
||||
"authentik/support.jwe": encrypt(generate()),
|
||||
"authentik/blueprint.yaml": exporter.export_to_string(),
|
||||
}
|
||||
with open(fileobj=fh, mode="w:gz") as tar:
|
||||
for path, file in files.items():
|
||||
info = TarInfo(path)
|
||||
info.size = len(file)
|
||||
tar.addfile(info, BytesIO(file.encode()))
|
||||
final_data = fh.getvalue()
|
||||
return final_data
|
||||
@@ -1,7 +1,12 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.api import LicenseViewSet
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.api import LicenseViewSet, SupportBundleView
|
||||
|
||||
api_urlpatterns = [
|
||||
("enterprise/license", LicenseViewSet),
|
||||
path(
|
||||
"enterprise/support_bundle/", SupportBundleView.as_view(), name="enterprise_support_bundle"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -100,6 +100,9 @@ elif [[ "$1" == "healthcheck" ]]; then
|
||||
elif [[ "$1" == "dump_config" ]]; then
|
||||
shift
|
||||
exec python -m authentik.lib.config $@
|
||||
elif [[ "$1" == "support" ]]; then
|
||||
wait_for_db
|
||||
exec python -m lifecycle.support
|
||||
elif [[ "$1" == "debug" ]]; then
|
||||
exec sleep infinity
|
||||
else
|
||||
|
||||
@@ -7,10 +7,11 @@ from os import environ, system
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from psycopg import Connection, Cursor, connect
|
||||
from psycopg import Connection, Cursor
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.config import CONFIG, django_db_config
|
||||
from lifecycle.wait_for_db import get_postgres
|
||||
|
||||
LOGGER = get_logger()
|
||||
ADV_LOCK_UID = 1000
|
||||
@@ -71,17 +72,7 @@ def release_lock(cursor: Cursor):
|
||||
|
||||
|
||||
def run_migrations():
|
||||
conn = connect(
|
||||
dbname=CONFIG.get("postgresql.name"),
|
||||
user=CONFIG.get("postgresql.user"),
|
||||
password=CONFIG.get("postgresql.password"),
|
||||
host=CONFIG.get("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
conn = get_postgres()
|
||||
curr = conn.cursor()
|
||||
try:
|
||||
wait_for_lock(curr)
|
||||
|
||||
111
lifecycle/support.py
Normal file
111
lifecycle/support.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import platform
|
||||
from hashlib import sha512
|
||||
from pprint import pprint
|
||||
from ssl import OPENSSL_VERSION
|
||||
from sys import version as python_version
|
||||
|
||||
from cryptography.exceptions import InternalError
|
||||
from cryptography.hazmat.backends.openssl.backend import backend
|
||||
from jwcrypto.common import json_encode
|
||||
from jwcrypto.jwe import JWE
|
||||
from jwcrypto.jwk import JWK
|
||||
from jwt import encode
|
||||
from psutil import cpu_count, virtual_memory
|
||||
from redis import Redis
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
from authentik.root.install_id import get_install_id_raw
|
||||
from lifecycle.wait_for_db import get_postgres, get_redis
|
||||
|
||||
try:
|
||||
backend._enable_fips()
|
||||
except InternalError:
|
||||
pass
|
||||
|
||||
|
||||
def get_version_history():
|
||||
with get_postgres() as postgres:
|
||||
cur = postgres.cursor()
|
||||
cur.execute("""SELECT "timestamp", "version", "build" FROM authentik_version_history;""")
|
||||
for x, y, z in cur.fetchall():
|
||||
yield (x.timestamp(), y, z)
|
||||
|
||||
|
||||
def get_postgres_version():
|
||||
with get_postgres() as postgres:
|
||||
cur = postgres.cursor()
|
||||
cur.execute("""SELECT version();""")
|
||||
return cur.fetchone()[0]
|
||||
|
||||
|
||||
def get_redis_version():
|
||||
redis: Redis = get_redis()
|
||||
version = redis.info()
|
||||
redis.close()
|
||||
return f"{version["redis_version"]} {version["redis_mode"]} {version["os"]}"
|
||||
|
||||
|
||||
def get_limited_config():
|
||||
return {
|
||||
"postgresql": {
|
||||
"host": CONFIG.get("postgresql.host"),
|
||||
},
|
||||
"redis": {
|
||||
"host": CONFIG.get("redis.host"),
|
||||
},
|
||||
"debug": CONFIG.get_bool("debug"),
|
||||
"log_level": CONFIG.get("log_level"),
|
||||
"error_reporting": {
|
||||
"enabled": CONFIG.get_bool("error_reporting.enabled"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def generate():
|
||||
payload = {
|
||||
"version": {
|
||||
"history": list(get_version_history()),
|
||||
"current": get_full_version(),
|
||||
"postgres": get_postgres_version(),
|
||||
"redis": get_redis_version(),
|
||||
"ssl": OPENSSL_VERSION,
|
||||
"python": python_version,
|
||||
},
|
||||
"env": get_env(),
|
||||
"install_id_hash": sha512(get_install_id_raw().encode("ascii")).hexdigest()[:16],
|
||||
"system": {
|
||||
"cpu": {"count": cpu_count()},
|
||||
"fips": backend._fips_enabled,
|
||||
"memory_bytes": virtual_memory().total,
|
||||
"architecture": platform.machine(),
|
||||
"platform": platform.platform(),
|
||||
"uname": " ".join(platform.uname()),
|
||||
},
|
||||
"config": get_limited_config(),
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def encrypt(raw):
|
||||
with open("authentik/enterprise/public.pem", "rb") as _key:
|
||||
key = JWK.from_pem(_key.read())
|
||||
jwe = JWE(
|
||||
encode(raw, "foo"),
|
||||
json_encode(
|
||||
{
|
||||
"alg": "ECDH-ES+A256KW",
|
||||
"enc": "A256CBC-HS512",
|
||||
"typ": "JWE",
|
||||
}
|
||||
),
|
||||
)
|
||||
jwe.add_recipient(key)
|
||||
return jwe.serialize(compact=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = generate()
|
||||
snippet = encrypt(data)
|
||||
pprint(data)
|
||||
@@ -13,23 +13,32 @@ from authentik.lib.config import CONFIG, redis_url
|
||||
CHECK_THRESHOLD = 30
|
||||
|
||||
|
||||
def get_postgres():
|
||||
return connect(
|
||||
dbname=CONFIG.get("postgresql.name"),
|
||||
user=CONFIG.get("postgresql.user"),
|
||||
password=CONFIG.get("postgresql.password"),
|
||||
host=CONFIG.get("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
|
||||
|
||||
def get_redis():
|
||||
url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db"))
|
||||
return Redis.from_url(url)
|
||||
|
||||
|
||||
def check_postgres():
|
||||
attempt = 0
|
||||
while True:
|
||||
if attempt >= CHECK_THRESHOLD:
|
||||
sysexit(1)
|
||||
try:
|
||||
conn = connect(
|
||||
dbname=CONFIG.refresh("postgresql.name"),
|
||||
user=CONFIG.refresh("postgresql.user"),
|
||||
password=CONFIG.refresh("postgresql.password"),
|
||||
host=CONFIG.refresh("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
conn = get_postgres()
|
||||
conn.cursor()
|
||||
break
|
||||
except OperationalError as exc:
|
||||
@@ -41,13 +50,12 @@ def check_postgres():
|
||||
|
||||
|
||||
def check_redis():
|
||||
url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db"))
|
||||
attempt = 0
|
||||
while True:
|
||||
if attempt >= CHECK_THRESHOLD:
|
||||
sysexit(1)
|
||||
try:
|
||||
redis = Redis.from_url(url)
|
||||
redis = get_redis()
|
||||
redis.ping()
|
||||
break
|
||||
except RedisError as exc:
|
||||
|
||||
@@ -48,6 +48,7 @@ dependencies = [
|
||||
"opencontainers==0.0.15",
|
||||
"packaging==25.0",
|
||||
"paramiko==3.5.1",
|
||||
"psutil==7.0.0",
|
||||
"psycopg[c,pool]==3.2.9",
|
||||
"pydantic==2.11.7",
|
||||
"pydantic-scim==0.0.8",
|
||||
|
||||
28
schema.yml
28
schema.yml
@@ -7006,6 +7006,34 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/support_bundle/:
|
||||
post:
|
||||
operationId: enterprise_support_bundle_create
|
||||
description: Generate a support bundle.
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/gzip:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/events/events/:
|
||||
get:
|
||||
operationId: events_events_list
|
||||
|
||||
17
uv.lock
generated
17
uv.lock
generated
@@ -211,6 +211,7 @@ dependencies = [
|
||||
{ name = "opencontainers" },
|
||||
{ name = "packaging" },
|
||||
{ name = "paramiko" },
|
||||
{ name = "psutil" },
|
||||
{ name = "psycopg", extra = ["c", "pool"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-scim" },
|
||||
@@ -310,6 +311,7 @@ requires-dist = [
|
||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
{ name = "paramiko", specifier = "==3.5.1" },
|
||||
{ name = "psutil", specifier = "==7.0.0" },
|
||||
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.2.9" },
|
||||
{ name = "pydantic", specifier = "==2.11.7" },
|
||||
{ name = "pydantic-scim", specifier = "==0.0.8" },
|
||||
@@ -2411,6 +2413,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.2.9"
|
||||
|
||||
@@ -261,6 +261,16 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||
>${msg("Go to Customer Portal")}</a
|
||||
>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-action-button
|
||||
class="pf-m-secondary pf-m-block"
|
||||
.apiRequest=${() => {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseSupportBundleCreate();
|
||||
}}
|
||||
>
|
||||
${msg("Create support bundle")}
|
||||
</ak-action-button>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<a target="_blank" href="https://docs.goauthentik.io/docs/enterprise/get-started"
|
||||
>${msg("Learn more")}</a
|
||||
|
||||
Reference in New Issue
Block a user