Merge branch 'main' into application-wizard-2-with-api-and-tests

* main: (58 commits)
  web: Replace ad-hoc toggle control with ak-toggle-group (#6470)
  blueprints: fix tag values not resolved correctly (#6653)
  web: bump @codemirror/lang-javascript from 6.1.9 to 6.2.0 in /web (#6647)
  core: bump ruff from 0.0.285 to 0.0.286 (#6649)
  web: bump the eslint group in /web with 1 update (#6646)
  web: bump @rollup/plugin-typescript from 11.1.2 to 11.1.3 in /web (#6648)
  core: bump python from 3.11.4-slim-bookworm to 3.11.5-slim-bookworm (#6650)
  web/admin: only show token expiry when token is set to expire (#6643)
  providers/proxy: fix JWKS url in embedded outpost (#6644)
  providers/oauth2: fix id_token being saved incorrectly leading to lost claims (#6645)
  web/user: only render expand element when required (#6641)
  root: re-fix docker build paths
  web/admin: set required flag to false for user attributes (#6418)
  root: fix docker build
  root: fix config loading for outposts (#6640)
  core: compile backend translations (#6639)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in nl on branch main (#6635)
  translate: Updates for file web/xliff/en.xlf in nl on branch main (#6634)
  core: fix filtering users by type attribute (#6638)
  web/elements: improve table error handling, prevent infinite loading … (#6636)
  ...
This commit is contained in:
Ken Sternberg 2023-08-28 13:29:58 -07:00
commit 93d7507d11
97 changed files with 12872 additions and 904 deletions

View file

@ -7,3 +7,4 @@ build/**
build_docs/**
Dockerfile
authentik/enterprise
blueprints/local

34
.github/workflows/gha-cache-cleanup.yml vendored Normal file
View file

@ -0,0 +1,34 @@
---
# See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup cache after PR is closed
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
# Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR; do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -5,6 +5,11 @@ on:
branches:
- main
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
publish-source-docs:
runs-on: ubuntu-latest
@ -15,6 +20,7 @@ jobs:
uses: ./.github/actions/setup
- name: generate docs
run: |
poetry run make migrate
poetry run ak build_source_docs
- name: Publish
uses: netlify/actions/cli@master

View file

@ -20,7 +20,7 @@ WORKDIR /work/web
RUN npm ci --include=dev && npm run build
# Stage 3: Poetry to requirements.txt export
FROM docker.io/python:3.11.4-slim-bullseye AS poetry-locker
FROM docker.io/python:3.11.5-slim-bookworm AS poetry-locker
WORKDIR /work
COPY ./pyproject.toml /work
@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy
FROM docker.io/golang:1.21.0-bullseye AS go-builder
FROM docker.io/golang:1.21.0-bookworm AS go-builder
WORKDIR /work
@ -39,12 +39,13 @@ COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt
COPY --from=web-builder /work/web/security.txt /work/web/security.txt
COPY ./cmd /work/cmd
COPY ./authentik/lib /work/authentik/lib
COPY ./web/static.go /work/web/static.go
COPY ./internal /work/internal
COPY ./go.mod /work/go.mod
COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/
RUN go build -o /work/bin/authentik ./cmd/server/
# Stage 5: MaxMind GeoIP
FROM ghcr.io/maxmind/geoipupdate:v6.0 as geoip
@ -61,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 6: Run
FROM docker.io/python:3.11.4-slim-bullseye AS final-image
FROM docker.io/python:3.11.5-slim-bookworm AS final-image
ARG GIT_BUILD_HASH
ARG VERSION
@ -104,7 +105,7 @@ COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle
COPY --from=go-builder /work/authentik /bin/authentik
COPY --from=go-builder /work/bin/authentik /bin/authentik
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/

View file

@ -27,6 +27,8 @@ To report a vulnerability, send an email to [security@goauthentik.io](mailto:se
authentik reserves the right to reclassify CVSS as necessary. To determine severity, we will use the CVSS calculator from NVD (https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The calculated CVSS score will then be translated into one of the following categories:
| Score | Severity |
| --- | --- |
| 0.0 | None |
| 0.1 3.9 | Low |
| 4.0 6.9 | Medium |

View file

@ -45,3 +45,8 @@ entries:
attrs:
name: "%(uid)s"
password: "%(uid)s"
- model: authentik_core.user
identifiers:
username: "%(uid)s-no-password"
attrs:
name: "%(uid)s"

View file

@ -36,6 +36,7 @@ entries:
model: authentik_policies_expression.expressionpolicy
- attrs:
attributes:
env_null: !Env [bar-baz, null]
policy_pk1:
!Format [
"%s-%s",

View file

@ -213,8 +213,9 @@ class TestBlueprintsV1(TransactionTestCase):
},
},
"nested_context": "context-nested-value",
"env_null": None,
}
)
).exists()
)
self.assertTrue(
OAuthSource.objects.filter(

View file

@ -51,3 +51,9 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
user: User = User.objects.filter(username=self.uid).first()
self.assertIsNotNone(user)
self.assertTrue(user.check_password(self.uid))
def test_user_null(self):
"""Test user"""
user: User = User.objects.filter(username=f"{self.uid}-no-password").first()
self.assertIsNotNone(user)
self.assertFalse(user.has_usable_password())

View file

@ -224,11 +224,11 @@ class Env(YAMLTag):
if isinstance(node, ScalarNode):
self.key = node.value
if isinstance(node, SequenceNode):
self.key = node.value[0].value
self.default = node.value[1].value
self.key = loader.construct_object(node.value[0])
self.default = loader.construct_object(node.value[1])
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
return getenv(self.key, self.default)
return getenv(self.key) or self.default
class Context(YAMLTag):
@ -243,8 +243,8 @@ class Context(YAMLTag):
if isinstance(node, ScalarNode):
self.key = node.value
if isinstance(node, SequenceNode):
self.key = node.value[0].value
self.default = node.value[1].value
self.key = loader.construct_object(node.value[0])
self.default = loader.construct_object(node.value[1])
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
value = self.default
@ -263,7 +263,7 @@ class Format(YAMLTag):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.format_string = node.value[0].value
self.format_string = loader.construct_object(node.value[0])
self.args = []
for raw_node in node.value[1:]:
self.args.append(loader.construct_object(raw_node))
@ -342,7 +342,7 @@ class Condition(YAMLTag):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.mode = node.value[0].value
self.mode = loader.construct_object(node.value[0])
self.args = []
for raw_node in node.value[1:]:
self.args.append(loader.construct_object(raw_node))
@ -417,7 +417,7 @@ class Enumerate(YAMLTag, YAMLTagContext):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.iterable = loader.construct_object(node.value[0])
self.output_body = node.value[1].value
self.output_body = loader.construct_object(node.value[1])
self.item_body = loader.construct_object(node.value[2])
self.__current_context: tuple[Any, Any] = tuple()

View file

@ -123,27 +123,35 @@ class UserSerializer(ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False)
self.fields["password"] = CharField(required=False, allow_null=True)
def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
instance: User = super().create(validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data:
instance.set_password(validated_data["password"])
instance.save()
self._set_password(instance, password)
return instance
def update(self, instance: User, validated_data: dict) -> User:
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
password = validated_data.pop("password", None)
instance = super().update(instance, validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data:
instance.set_password(validated_data["password"])
instance.save()
self._set_password(instance, password)
return instance
def _set_password(self, instance: User, password: Optional[str]):
"""Set password of user if we're in a blueprint context, and if it's an empty
string then use an unusable password"""
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and password:
instance.set_password(password)
instance.save()
if len(instance.password) == 0:
instance.set_unusable_password()
instance.save()
def validate_path(self, path: str) -> str:
"""Validate path"""
if path[:1] == "/" or path[-1] == "/":
@ -309,7 +317,7 @@ class UsersFilter(FilterSet):
path = CharFilter(field_name="path")
path_startswith = CharFilter(field_name="path", lookup_expr="startswith")
type = MultipleChoiceFilter(field_name="type")
type = MultipleChoiceFilter(choices=UserTypes.choices, field_name="type")
groups_by_name = ModelMultipleChoiceFilter(
field_name="ak_groups__name",

View file

@ -28,6 +28,19 @@ class TestUsersAPI(APITestCase):
self.admin = create_test_admin_user()
self.user = User.objects.create(username="test-user")
def test_filter_type(self):
"""Test API filtering by type"""
self.client.force_login(self.admin)
user = create_test_admin_user(type=UserTypes.EXTERNAL)
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"type": UserTypes.EXTERNAL,
"username": user.username,
},
)
self.assertEqual(response.status_code, 200)
def test_metrics(self):
"""Test user's metrics"""
self.client.force_login(self.admin)

View file

@ -21,7 +21,7 @@ def create_test_flow(
)
def create_test_admin_user(name: Optional[str] = None) -> User:
def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User:
"""Generate a test-admin user"""
uid = generate_id(20) if not name else name
group = Group.objects.create(name=uid, is_superuser=True)
@ -29,6 +29,7 @@ def create_test_admin_user(name: Optional[str] = None) -> User:
username=uid,
name=uid,
email=f"{uid}@goauthentik.io",
**kwargs,
)
user.set_password(uid)
user.save()
@ -36,12 +37,12 @@ def create_test_admin_user(name: Optional[str] = None) -> User:
return user
def create_test_tenant() -> Tenant:
def create_test_tenant(**kwargs) -> Tenant:
"""Generate a test tenant, removing all other tenants to make sure this one
matches."""
uid = generate_id(20)
Tenant.objects.all().delete()
return Tenant.objects.create(domain=uid, default=True)
return Tenant.objects.create(domain=uid, default=True, **kwargs)
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:

View file

@ -35,13 +35,13 @@ class LicenseSerializer(ModelSerializer):
"name",
"key",
"expiry",
"users",
"internal_users",
"external_users",
]
extra_kwargs = {
"name": {"read_only": True},
"expiry": {"read_only": True},
"users": {"read_only": True},
"internal_users": {"read_only": True},
"external_users": {"read_only": True},
}
@ -49,7 +49,7 @@ class LicenseSerializer(ModelSerializer):
class LicenseSummary(PassiveSerializer):
"""Serializer for license status"""
users = IntegerField(required=True)
internal_users = IntegerField(required=True)
external_users = IntegerField(required=True)
valid = BooleanField()
show_admin_warning = BooleanField()
@ -62,9 +62,9 @@ class LicenseSummary(PassiveSerializer):
class LicenseForecastSerializer(PassiveSerializer):
"""Serializer for license forecast"""
users = IntegerField(required=True)
internal_users = IntegerField(required=True)
external_users = IntegerField(required=True)
forecasted_users = IntegerField(required=True)
forecasted_internal_users = IntegerField(required=True)
forecasted_external_users = IntegerField(required=True)
@ -111,7 +111,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
latest_valid = datetime.fromtimestamp(total.exp)
response = LicenseSummary(
data={
"users": total.users,
"internal_users": total.internal_users,
"external_users": total.external_users,
"valid": total.is_valid(),
"show_admin_warning": show_admin_warning,
@ -135,8 +135,8 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
def forecast(self, request: Request) -> Response:
"""Forecast how many users will be required in a year"""
last_month = now() - timedelta(days=30)
# Forecast for default users
users_in_last_month = User.objects.filter(
# Forecast for internal users
internal_in_last_month = User.objects.filter(
type=UserTypes.INTERNAL, date_joined__gte=last_month
).count()
# Forecast for external users
@ -144,9 +144,9 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
forecast_for_months = 12
response = LicenseForecastSerializer(
data={
"users": LicenseKey.get_default_user_count(),
"internal_users": LicenseKey.get_default_user_count(),
"external_users": LicenseKey.get_external_user_count(),
"forecasted_users": (users_in_last_month * forecast_for_months),
"forecasted_internal_users": (internal_in_last_month * forecast_for_months),
"forecasted_external_users": (external_in_last_month * forecast_for_months),
}
)

View file

@ -0,0 +1,29 @@
# Generated by Django 4.2.4 on 2023-08-23 10:06
import django.contrib.postgres.indexes
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_enterprise", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name="license",
old_name="users",
new_name="internal_users",
),
migrations.AlterField(
model_name="license",
name="key",
field=models.TextField(),
),
migrations.AddIndex(
model_name="license",
index=django.contrib.postgres.indexes.HashIndex(
fields=["key"], name="authentik_e_key_523e13_hash"
),
),
]

View file

@ -11,6 +11,7 @@ from uuid import uuid4
from cryptography.exceptions import InvalidSignature
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
from dacite import from_dict
from django.contrib.postgres.indexes import HashIndex
from django.db import models
from django.db.models.query import QuerySet
from django.utils.timezone import now
@ -46,8 +47,8 @@ class LicenseKey:
exp: int
name: str
users: int
external_users: int
internal_users: int = 0
external_users: int = 0
flags: list[LicenseFlags] = field(default_factory=list)
@staticmethod
@ -87,7 +88,7 @@ class LicenseKey:
active_licenses = License.objects.filter(expiry__gte=now())
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in active_licenses:
total.users += lic.users
total.internal_users += lic.internal_users
total.external_users += lic.external_users
exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0:
@ -123,7 +124,7 @@ class LicenseKey:
Only checks the current count, no historical data is checked"""
default_users = self.get_default_user_count()
if default_users > self.users:
if default_users > self.internal_users:
return False
active_users = self.get_external_user_count()
if active_users > self.external_users:
@ -153,11 +154,11 @@ class License(models.Model):
"""An authentik enterprise license"""
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
key = models.TextField(unique=True)
key = models.TextField()
name = models.TextField()
expiry = models.DateTimeField()
users = models.BigIntegerField()
internal_users = models.BigIntegerField()
external_users = models.BigIntegerField()
@property
@ -165,6 +166,9 @@ class License(models.Model):
"""Get parsed license status"""
return LicenseKey.validate(self.key)
class Meta:
indexes = (HashIndex(fields=("key",)),)
def usage_expiry():
"""Keep license usage records for 3 months"""

View file

@ -13,6 +13,6 @@ def pre_save_license(sender: type[License], instance: License, **_):
"""Extract data from license jwt and save it into model"""
status = instance.status
instance.name = status.name
instance.users = status.users
instance.internal_users = status.internal_users
instance.external_users = status.external_users
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())

View file

@ -23,7 +23,7 @@ class TestEnterpriseLicense(TestCase):
aud="",
exp=_exp,
name=generate_id(),
users=100,
internal_users=100,
external_users=100,
)
),
@ -32,7 +32,7 @@ class TestEnterpriseLicense(TestCase):
"""Check license verification"""
lic = License.objects.create(key=generate_id())
self.assertTrue(lic.status.is_valid())
self.assertEqual(lic.users, 100)
self.assertEqual(lic.internal_users, 100)
def test_invalid(self):
"""Test invalid license"""
@ -46,7 +46,7 @@ class TestEnterpriseLicense(TestCase):
aud="",
exp=_exp,
name=generate_id(),
users=100,
internal_users=100,
external_users=100,
)
),
@ -58,7 +58,7 @@ class TestEnterpriseLicense(TestCase):
lic2 = License.objects.create(key=generate_id())
self.assertTrue(lic2.status.is_valid())
total = LicenseKey.get_total()
self.assertEqual(total.users, 200)
self.assertEqual(total.internal_users, 200)
self.assertEqual(total.external_users, 200)
self.assertEqual(total.exp, _exp)
self.assertTrue(total.is_valid())

10
authentik/lib/config.go Normal file
View file

@ -0,0 +1,10 @@
package lib
import _ "embed"
//go:embed default.yml
var defaultConfig []byte
func DefaultConfig() []byte {
return defaultConfig
}

View file

@ -11,7 +11,11 @@ postgresql:
listen:
listen_http: 0.0.0.0:9000
listen_https: 0.0.0.0:9443
listen_ldap: 0.0.0.0:3389
listen_ldaps: 0.0.0.0:6636
listen_radius: 0.0.0.0:1812
listen_metrics: 0.0.0.0:9300
listen_debug: 0.0.0.0:9900
trusted_proxy_cidrs:
- 127.0.0.0/8
- 10.0.0.0/8
@ -32,6 +36,9 @@ redis:
cache_timeout_policies: 300
cache_timeout_reputation: 300
paths:
media: ./media
debug: false
remote_debug: false

View file

@ -2,6 +2,7 @@
import base64
import binascii
import json
from dataclasses import asdict
from functools import cached_property
from hashlib import sha256
from typing import Any, Optional
@ -358,7 +359,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
@id_token.setter
def id_token(self, value: IDToken):
self.token = value.to_access_token(self.provider)
self._id_token = json.dumps(value.to_dict())
self._id_token = json.dumps(asdict(value))
@property
def at_hash(self):
@ -400,7 +401,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
@id_token.setter
def id_token(self, value: IDToken):
self._id_token = json.dumps(value.to_dict())
self._id_token = json.dumps(asdict(value))
@property
def serializer(self) -> Serializer:

View file

@ -151,6 +151,14 @@ class TestTokenClientCredentials(OAuthTestCase):
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
jwt = decode(
body["id_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_password(self):
"""test successful (password grant)"""

View file

@ -68,7 +68,7 @@ class SCIMClient(Generic[T, SchemaType]):
"""Get Service provider config"""
default_config = ServiceProviderConfiguration.default()
try:
return ServiceProviderConfiguration.parse_obj(
return ServiceProviderConfiguration.model_validate(
self._request("GET", "/ServiceProviderConfig")
)
except (ValidationError, SCIMRequestException) as exc:

View file

@ -74,7 +74,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if not raw_scim_group:
raise StopSync(ValueError("No group mappings configured"), obj)
try:
scim_group = SCIMGroupSchema.parse_obj(delete_none_values(raw_scim_group))
scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if not scim_group.externalId:
@ -99,7 +99,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
response = self._request(
"POST",
"/Groups",
data=scim_group.json(
json=scim_group.model_dump(
mode="json",
exclude_unset=True,
),
)
@ -113,7 +114,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return self._request(
"PUT",
f"/Groups/{scim_group.id}",
data=scim_group.json(
json=scim_group.model_dump(
mode="json",
exclude_unset=True,
),
)
@ -160,7 +162,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
*ops: PatchOperation,
):
req = PatchRequest(Operations=ops)
self._request("PATCH", f"/Groups/{group_id}", data=req.json())
self._request(
"PATCH",
f"/Groups/{group_id}",
json=req.model_dump(
mode="json",
),
)
def _patch_add_users(self, group: Group, users_set: set[int]):
"""Add users in users_set to group"""

View file

@ -52,7 +52,7 @@ class ServiceProviderConfiguration(BaseServiceProviderConfiguration):
class PatchRequest(BasePatchRequest):
"""PatchRequest which correctly sets schemas"""
schemas: tuple[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
schemas: tuple[str] = ("urn:ietf:params:scim:api:messages:2.0:PatchOp",)
class SCIMError(BaseSCIMError):

View file

@ -64,7 +64,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if not raw_scim_user:
raise StopSync(ValueError("No user mappings configured"), obj)
try:
scim_user = SCIMUserSchema.parse_obj(delete_none_values(raw_scim_user))
scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if not scim_user.externalId:
@ -77,7 +77,8 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
response = self._request(
"POST",
"/Users",
data=scim_user.json(
json=scim_user.model_dump(
mode="json",
exclude_unset=True,
),
)
@ -90,7 +91,8 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
self._request(
"PUT",
f"/Users/{connection.id}",
data=scim_user.json(
json=scim_user.model_dump(
mode="json",
exclude_unset=True,
),
)

View file

@ -47,6 +47,7 @@ class SCIMMembershipTests(TestCase):
def test_member_add(self):
"""Test member add"""
config = ServiceProviderConfiguration.default()
# pylint: disable=assigning-non-slot
config.patch.supported = True
user_scim_id = generate_id()
group_scim_id = generate_id()
@ -60,7 +61,7 @@ class SCIMMembershipTests(TestCase):
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.dict(),
json=config.model_dump(),
)
mocker.post(
"https://localhost/Users",
@ -104,7 +105,7 @@ class SCIMMembershipTests(TestCase):
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.dict(),
json=config.model_dump(),
)
mocker.patch(
f"https://localhost/Groups/{group_scim_id}",
@ -131,6 +132,7 @@ class SCIMMembershipTests(TestCase):
def test_member_remove(self):
"""Test member remove"""
config = ServiceProviderConfiguration.default()
# pylint: disable=assigning-non-slot
config.patch.supported = True
user_scim_id = generate_id()
group_scim_id = generate_id()
@ -144,7 +146,7 @@ class SCIMMembershipTests(TestCase):
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.dict(),
json=config.model_dump(),
)
mocker.post(
"https://localhost/Users",
@ -188,7 +190,7 @@ class SCIMMembershipTests(TestCase):
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.dict(),
json=config.model_dump(),
)
mocker.patch(
f"https://localhost/Groups/{group_scim_id}",
@ -215,7 +217,7 @@ class SCIMMembershipTests(TestCase):
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.dict(),
json=config.model_dump(),
)
mocker.patch(
f"https://localhost/Groups/{group_scim_id}",

View file

@ -62,7 +62,8 @@ class OAuthSourceSerializer(SourceSerializer):
well_known_config = session.get(well_known)
well_known_config.raise_for_status()
except RequestException as exc:
raise ValidationError(exc.response.text)
text = exc.response.text if exc.response else str(exc)
raise ValidationError(text)
config = well_known_config.json()
try:
attrs["authorization_url"] = config["authorization_endpoint"]
@ -78,7 +79,8 @@ class OAuthSourceSerializer(SourceSerializer):
jwks_config = session.get(jwks_url)
jwks_config.raise_for_status()
except RequestException as exc:
raise ValidationError(exc.response.text)
text = exc.response.text if exc.response else str(exc)
raise ValidationError(text)
config = jwks_config.json()
attrs["oidc_jwks"] = config

View file

@ -1,5 +1,4 @@
"""Validation stage challenge checking"""
from json import dumps, loads
from typing import Optional
from urllib.parse import urlencode
@ -17,7 +16,6 @@ from webauthn.authentication.generate_authentication_options import generate_aut
from webauthn.authentication.verify_authentication_response import verify_authentication_response
from webauthn.helpers.base64url_to_bytes import base64url_to_bytes
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.options_to_json import options_to_json
from webauthn.helpers.structs import AuthenticationCredential
from authentik.core.api.utils import PassiveSerializer
@ -68,7 +66,12 @@ def get_webauthn_challenge_without_user(
)
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
return loads(options_to_json(authentication_options))
return authentication_options.model_dump(
mode="json",
by_alias=True,
exclude_unset=False,
exclude_none=True,
)
def get_webauthn_challenge(
@ -93,7 +96,12 @@ def get_webauthn_challenge(
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
return loads(options_to_json(authentication_options))
return authentication_options.model_dump(
mode="json",
by_alias=True,
exclude_unset=False,
exclude_none=True,
)
def select_challenge(request: HttpRequest, device: Device):
@ -144,7 +152,7 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
try:
authentication_verification = verify_authentication_response(
credential=AuthenticationCredential.parse_raw(dumps(data)),
credential=AuthenticationCredential.model_validate(data),
expected_challenge=challenge,
expected_rp_id=get_rp_id(request),
expected_origin=get_origin(request),

View file

@ -234,14 +234,12 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"assertionClientExtensions": "{}",
"response": {
"clientDataJSON": (
(
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvN"
"Wx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQW"
"dPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWx"
"ob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2Jl"
"X2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2Fpb"
"nN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
),
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvN"
"Wx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQW"
"dPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWx"
"ob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2Jl"
"X2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2Fpb"
"nN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
),
"signature": (
"MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI"
@ -306,14 +304,12 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"assertionClientExtensions": "{}",
"response": {
"clientDataJSON": (
(
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2Wlhv"
"NWx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGcz"
"QWdPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9j"
"YWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2Fu"
"X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBh"
"Z2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
),
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2Wlhv"
"NWx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGcz"
"QWdPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9j"
"YWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2Fu"
"X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBh"
"Z2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
),
"signature": (
"MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI"

View file

@ -1,13 +1,10 @@
"""WebAuthn stage"""
from json import dumps, loads
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.options_to_json import options_to_json
from webauthn.helpers.structs import (
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
@ -55,7 +52,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
try:
registration: VerifiedRegistration = verify_registration_response(
credential=RegistrationCredential.parse_raw(dumps(response)),
credential=RegistrationCredential.model_validate(response),
expected_challenge=challenge,
expected_rp_id=get_rp_id(self.request),
expected_origin=get_origin(self.request),
@ -108,7 +105,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
return AuthenticatorWebAuthnChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"registration": loads(options_to_json(registration_options)),
"registration": registration_options.model_dump(
mode="json",
by_alias=True,
exclude_unset=False,
exclude_none=True,
),
}
)

View file

@ -8471,7 +8471,10 @@
"title": "Type"
},
"password": {
"type": "string",
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Password"
}

6
go.mod
View file

@ -13,20 +13,20 @@ require (
github.com/go-openapi/runtime v0.26.0
github.com/go-openapi/strfmt v0.21.7
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jellydator/ttlcache/v3 v3.0.1
github.com/jellydator/ttlcache/v3 v3.1.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.16.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
goauthentik.io/api/v3 v3.2023061.12
goauthentik.io/api/v3 v3.2023061.13
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.11.0
golang.org/x/sync v0.3.0

15
go.sum
View file

@ -859,8 +859,9 @@ github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@ -903,8 +904,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU=
github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@ -1069,9 +1070,9 @@ go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+go
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023061.12 h1:VtxeDeOpEtO8DXHVSYnhVQwtD80zIrwWSnI8b7lJlb0=
goauthentik.io/api/v3 v3.2023061.12/go.mod h1:sP1/Ak/vGw96xNgpyoObHgXfyAElcTN5CbbC+VdPQXk=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
goauthentik.io/api/v3 v3.2023061.13 h1:0V5XrryJdMrOug/5wWazmH+D3Y/dDGPyLDhWcbJ5Gm0=
goauthentik.io/api/v3 v3.2023061.13/go.mod h1:sP1/Ak/vGw96xNgpyoObHgXfyAElcTN5CbbC+VdPQXk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@ -1133,7 +1134,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -1454,7 +1454,6 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,67 +1,102 @@
package config
import (
_ "embed"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
env "github.com/Netflix/go-env"
log "github.com/sirupsen/logrus"
"goauthentik.io/authentik/lib"
"gopkg.in/yaml.v2"
)
var cfg *Config
const defaultConfigPath = "./authentik/lib/default.yml"
func getConfigPaths() []string {
configPaths := []string{defaultConfigPath, "/etc/authentik/config.yml", ""}
globConfigPaths, _ := filepath.Glob("/etc/authentik/config.d/*.yml")
configPaths = append(configPaths, globConfigPaths...)
environment := "local"
if v, ok := os.LookupEnv("AUTHENTIK_ENV"); ok {
environment = v
}
computedConfigPaths := []string{}
for _, path := range configPaths {
path, err := filepath.Abs(path)
if err != nil {
continue
}
if stat, err := os.Stat(path); err == nil {
if !stat.IsDir() {
computedConfigPaths = append(computedConfigPaths, path)
} else {
envPaths := []string{
filepath.Join(path, environment+".yml"),
filepath.Join(path, environment+".env.yml"),
}
for _, envPath := range envPaths {
if stat, err = os.Stat(envPath); err == nil && !stat.IsDir() {
computedConfigPaths = append(computedConfigPaths, envPath)
}
}
}
}
}
return computedConfigPaths
}
func Get() *Config {
if cfg == nil {
c := defaultConfig()
c.Setup("./authentik/lib/default.yml", "/etc/authentik/config.yml", "./local.env.yml")
c := &Config{}
c.Setup(getConfigPaths()...)
cfg = c
}
return cfg
}
func defaultConfig() *Config {
return &Config{
Debug: false,
Listen: ListenConfig{
HTTP: "0.0.0.0:9000",
HTTPS: "0.0.0.0:9443",
LDAP: "0.0.0.0:3389",
LDAPS: "0.0.0.0:6636",
Radius: "0.0.0.0:1812",
Metrics: "0.0.0.0:9300",
Debug: "0.0.0.0:9900",
},
Paths: PathsConfig{
Media: "./media",
},
LogLevel: "info",
ErrorReporting: ErrorReportingConfig{
Enabled: false,
SampleRate: 1,
},
}
}
func (c *Config) Setup(paths ...string) {
// initially try to load the default config which is compiled in
err := c.LoadConfig(lib.DefaultConfig())
// this should never fail
if err != nil {
panic(fmt.Errorf("failed to load inbuilt config: %v", err))
}
log.WithField("path", "inbuilt-default").Debug("Loaded config")
for _, path := range paths {
err := c.LoadConfig(path)
err := c.LoadConfigFromFile(path)
if err != nil {
log.WithError(err).Info("failed to load config, skipping")
}
}
err := c.fromEnv()
err = c.fromEnv()
if err != nil {
log.WithError(err).Info("failed to load env vars")
}
c.configureLogger()
}
func (c *Config) LoadConfig(path string) error {
func (c *Config) LoadConfig(raw []byte) error {
err := yaml.Unmarshal(raw, c)
if err != nil {
return fmt.Errorf("failed to parse YAML: %w", err)
}
c.walkScheme(c)
return nil
}
func (c *Config) LoadConfigFromFile(path string) error {
raw, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@ -69,11 +104,10 @@ func (c *Config) LoadConfig(path string) error {
}
return fmt.Errorf("failed to load config file: %w", err)
}
err = yaml.Unmarshal(raw, c)
err = c.LoadConfig(raw)
if err != nil {
return fmt.Errorf("failed to parse YAML: %w", err)
return err
}
c.walkScheme(c)
log.WithField("path", path).Debug("Loaded config")
return nil
}

View file

@ -55,6 +55,8 @@ type Application struct {
errorTemplates *template.Template
authHeaderCache *ttlcache.Cache[string, Claims]
isEmbedded bool
}
type Server interface {
@ -86,15 +88,15 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
CallbackSignature: []string{"true"},
}.Encode()
managed := false
isEmbedded := false
if m := server.API().Outpost.Managed.Get(); m != nil {
managed = *m == "goauthentik.io/outposts/embedded"
isEmbedded = *m == "goauthentik.io/outposts/embedded"
}
// Configure an OpenID Connect aware OAuth2 client.
endpoint := GetOIDCEndpoint(
p,
server.API().Outpost.Config["authentik_host"].(string),
managed,
isEmbedded,
)
verifier := oidc.NewVerifier(endpoint.Issuer, ks, &oidc.Config{
@ -132,6 +134,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
ak: server.API(),
authHeaderCache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, Claims]()),
srv: server,
isEmbedded: isEmbedded,
}
go a.authHeaderCache.Start()
a.sessions = a.getStore(p, externalHost)

View file

@ -30,6 +30,7 @@ func updateURL(rawUrl string, scheme string, host string) string {
func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint {
authUrl := p.OidcConfiguration.AuthorizationEndpoint
endUrl := p.OidcConfiguration.EndSessionEndpoint
jwksUri := p.OidcConfiguration.JwksUri
issuer := p.OidcConfiguration.Issuer
ep := OIDCEndpoint{
Endpoint: oauth2.Endpoint{
@ -38,10 +39,14 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
AuthStyle: oauth2.AuthStyleInParams,
},
EndSessionEndpoint: endUrl,
JwksUri: p.OidcConfiguration.JwksUri,
JwksUri: jwksUri,
TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint,
Issuer: issuer,
}
aku, err := url.Parse(authentikHost)
if err != nil {
return ep
}
// For the embedded outpost, we use the configure `authentik_host` for the browser URLs
// and localhost (which is what we've got from the API) for backchannel URLs
//
@ -51,27 +56,24 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
if !embedded && hostBrowser == "" {
return ep
}
var newHost *url.URL
var newHost *url.URL = aku
var newBrowserHost *url.URL
if embedded {
if authentikHost == "" {
log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.")
return ep
}
aku, err := url.Parse(authentikHost)
if err != nil {
return ep
}
newHost = aku
newBrowserHost = aku
} else if hostBrowser != "" {
aku, err := url.Parse(hostBrowser)
browser, err := url.Parse(hostBrowser)
if err != nil {
return ep
}
newHost = aku
newBrowserHost = browser
}
// Update all browser-accessed URLs to use the new host and scheme
ep.AuthURL = updateURL(authUrl, newHost.Scheme, newHost.Host)
ep.EndSessionEndpoint = updateURL(endUrl, newHost.Scheme, newHost.Host)
ep.AuthURL = updateURL(authUrl, newBrowserHost.Scheme, newBrowserHost.Host)
ep.EndSessionEndpoint = updateURL(endUrl, newBrowserHost.Scheme, newBrowserHost.Host)
// Update issuer to use the same host and scheme, which would normally break as we don't
// change the token URL here, but the token HTTP transport overwrites the Host header
//
@ -79,6 +81,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
// is routed correctly
if embedded {
ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host)
ep.JwksUri = updateURL(jwksUri, newHost.Scheme, newHost.Host)
}
return ep
}

View file

@ -82,7 +82,7 @@ func TestEndpointEmbedded(t *testing.T) {
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/", ep.Issuer)
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
}

View file

@ -29,7 +29,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
// Add one to the validity to ensure we don't have a session with indefinite length
maxAge = int(*t) + 1
}
if config.Get().Redis.Host != "" {
if a.isEmbedded {
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.DB))
if err != nil {
panic(err)

View file

@ -1,5 +1,5 @@
# Stage 1: Build
FROM docker.io/golang:1.21.0-bullseye AS builder
FROM docker.io/golang:1.21.0-bookworm AS builder
WORKDIR /go/src/goauthentik.io

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-17 17:37+0000\n"
"POT-Creation-Date: 2023-08-23 10:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -98,125 +98,125 @@ msgstr ""
msgid "Users added to this group will be superusers."
msgstr ""
#: authentik/core/models.py:162
#: authentik/core/models.py:142
msgid "User's display name."
msgstr ""
#: authentik/core/models.py:256 authentik/providers/oauth2/models.py:294
#: authentik/core/models.py:268 authentik/providers/oauth2/models.py:294
msgid "User"
msgstr ""
#: authentik/core/models.py:257
#: authentik/core/models.py:269
msgid "Users"
msgstr ""
#: authentik/core/models.py:270
#: authentik/core/models.py:282
msgid ""
"Flow used for authentication when the associated application is accessed by "
"an un-authenticated user."
msgstr ""
#: authentik/core/models.py:280
#: authentik/core/models.py:292
msgid "Flow used when authorizing this provider."
msgstr ""
#: authentik/core/models.py:292
#: authentik/core/models.py:304
msgid ""
"Accessed from applications; optional backchannel providers for protocols "
"like LDAP and SCIM."
msgstr ""
#: authentik/core/models.py:347
#: authentik/core/models.py:359
msgid "Application's display Name."
msgstr ""
#: authentik/core/models.py:348
#: authentik/core/models.py:360
msgid "Internal application name, used in URLs."
msgstr ""
#: authentik/core/models.py:360
#: authentik/core/models.py:372
msgid "Open launch URL in a new browser tab or window."
msgstr ""
#: authentik/core/models.py:424
#: authentik/core/models.py:436
msgid "Application"
msgstr ""
#: authentik/core/models.py:425
#: authentik/core/models.py:437
msgid "Applications"
msgstr ""
#: authentik/core/models.py:431
#: authentik/core/models.py:443
msgid "Use the source-specific identifier"
msgstr ""
#: authentik/core/models.py:433
#: authentik/core/models.py:445
msgid ""
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
msgstr ""
#: authentik/core/models.py:437
#: authentik/core/models.py:449
msgid ""
"Use the user's email address, but deny enrollment when the email address "
"already exists."
msgstr ""
#: authentik/core/models.py:440
#: authentik/core/models.py:452
msgid ""
"Link to a user with identical username. Can have security implications when "
"a username is used with another source."
msgstr ""
#: authentik/core/models.py:444
#: authentik/core/models.py:456
msgid ""
"Use the user's username, but deny enrollment when the username already "
"exists."
msgstr ""
#: authentik/core/models.py:451
#: authentik/core/models.py:463
msgid "Source's display Name."
msgstr ""
#: authentik/core/models.py:452
#: authentik/core/models.py:464
msgid "Internal source name, used in URLs."
msgstr ""
#: authentik/core/models.py:471
#: authentik/core/models.py:483
msgid "Flow to use when authenticating existing users."
msgstr ""
#: authentik/core/models.py:480
#: authentik/core/models.py:492
msgid "Flow to use when enrolling new users."
msgstr ""
#: authentik/core/models.py:488
#: authentik/core/models.py:500
msgid ""
"How the source determines if an existing user should be authenticated or a "
"new user enrolled."
msgstr ""
#: authentik/core/models.py:660
#: authentik/core/models.py:672
msgid "Token"
msgstr ""
#: authentik/core/models.py:661
#: authentik/core/models.py:673
msgid "Tokens"
msgstr ""
#: authentik/core/models.py:702
#: authentik/core/models.py:714
msgid "Property Mapping"
msgstr ""
#: authentik/core/models.py:703
#: authentik/core/models.py:715
msgid "Property Mappings"
msgstr ""
#: authentik/core/models.py:738
#: authentik/core/models.py:750
msgid "Authenticated Session"
msgstr ""
#: authentik/core/models.py:739
#: authentik/core/models.py:751
msgid "Authenticated Sessions"
msgstr ""

Binary file not shown.

File diff suppressed because it is too large Load diff

250
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -150,6 +150,17 @@ files = [
[package.dependencies]
vine = ">=5.0.0"
[[package]]
name = "annotated-types"
version = "0.5.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.7"
files = [
{file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"},
{file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"},
]
[[package]]
name = "anyio"
version = "3.7.1"
@ -2674,56 +2685,141 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.12"
description = "Data validation and settings management using python type hints"
version = "2.3.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"},
{file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"},
{file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"},
{file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"},
{file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"},
{file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"},
{file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"},
{file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"},
{file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"},
{file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"},
{file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"},
{file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"},
{file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"},
{file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"},
{file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"},
{file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"},
{file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"},
{file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"},
{file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"},
{file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"},
{file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"},
{file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"},
{file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"},
{file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"},
{file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"},
{file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"},
{file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"},
{file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"},
{file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"},
{file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"},
{file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"},
{file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"},
{file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"},
{file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"},
{file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"},
{file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"},
{file = "pydantic-2.3.0-py3-none-any.whl", hash = "sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81"},
{file = "pydantic-2.3.0.tar.gz", hash = "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d"},
]
[package.dependencies]
email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""}
typing-extensions = ">=4.2.0"
annotated-types = ">=0.4.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.6.3"
typing-extensions = ">=4.6.1"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.6.3"
description = ""
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic_core-2.6.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad"},
{file = "pydantic_core-2.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728"},
{file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb"},
{file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd"},
{file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e"},
{file = "pydantic_core-2.6.3-cp310-none-win32.whl", hash = "sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7"},
{file = "pydantic_core-2.6.3-cp310-none-win_amd64.whl", hash = "sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad"},
{file = "pydantic_core-2.6.3-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973"},
{file = "pydantic_core-2.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da"},
{file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b"},
{file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6"},
{file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50"},
{file = "pydantic_core-2.6.3-cp311-none-win32.whl", hash = "sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8"},
{file = "pydantic_core-2.6.3-cp311-none-win_amd64.whl", hash = "sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950"},
{file = "pydantic_core-2.6.3-cp311-none-win_arm64.whl", hash = "sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2"},
{file = "pydantic_core-2.6.3-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5"},
{file = "pydantic_core-2.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5"},
{file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282"},
{file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d"},
{file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa"},
{file = "pydantic_core-2.6.3-cp312-none-win32.whl", hash = "sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1"},
{file = "pydantic_core-2.6.3-cp312-none-win_amd64.whl", hash = "sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881"},
{file = "pydantic_core-2.6.3-cp312-none-win_arm64.whl", hash = "sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9"},
{file = "pydantic_core-2.6.3-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b"},
{file = "pydantic_core-2.6.3-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5"},
{file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42"},
{file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58"},
{file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6"},
{file = "pydantic_core-2.6.3-cp37-none-win32.whl", hash = "sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b"},
{file = "pydantic_core-2.6.3-cp37-none-win_amd64.whl", hash = "sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525"},
{file = "pydantic_core-2.6.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b"},
{file = "pydantic_core-2.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db"},
{file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170"},
{file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec"},
{file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb"},
{file = "pydantic_core-2.6.3-cp38-none-win32.whl", hash = "sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc"},
{file = "pydantic_core-2.6.3-cp38-none-win_amd64.whl", hash = "sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378"},
{file = "pydantic_core-2.6.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465"},
{file = "pydantic_core-2.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f"},
{file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3"},
{file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76"},
{file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef"},
{file = "pydantic_core-2.6.3-cp39-none-win32.whl", hash = "sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a"},
{file = "pydantic_core-2.6.3-cp39-none-win_amd64.whl", hash = "sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b"},
{file = "pydantic_core-2.6.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7"},
{file = "pydantic_core-2.6.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483"},
{file = "pydantic_core-2.6.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d"},
{file = "pydantic_core-2.6.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149"},
{file = "pydantic_core-2.6.3.tar.gz", hash = "sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-scim"
@ -3309,28 +3405,28 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.0.285"
version = "0.0.286"
description = "An extremely fast Python linter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.285-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:72a3a0936369b986b0e959f9090206ed3c18f9e5e439ea5b8e6867c6707aded5"},
{file = "ruff-0.0.285-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0d9ab6ad16742eb78919e0fba09f914f042409df40ad63423c34bb20d350162a"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c48926156288b8ac005eb1db5e77c15e8a37309ae49d9fb6771d5cf5f777590"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d2a60c102e7a5e147b58fc2cbea12a563c565383effc527c987ea2086a05742"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b02aae62f922d088bb01943e1dbd861688ada13d735b78b8348a7d90121fd292"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f572c4296d8c7ddd22c3204de4031965be524fdd1fdaaef273945932912b28c5"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80effdf4fe69763d69eb4ab9443e186fd09e668b59fe70ba4b49f4c077d15a1b"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5977ce304da35c263f5e082901bd7ac0bd2be845a8fcfd1a29e4d6680cddb307"},
{file = "ruff-0.0.285-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a087712d474fa17b915d7cb9ef807e1256182b12ddfafb105eb00aeee48d1a"},
{file = "ruff-0.0.285-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7ce67736cd8dfe97162d1e7adfc2d9a1bac0efb9aaaff32e4042c7cde079f54b"},
{file = "ruff-0.0.285-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5473a4c6cac34f583bff08c5f63b8def5599a0ea4dc96c0302fbd2cc0b3ecbad"},
{file = "ruff-0.0.285-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6b1c961d608d373a032f047a20bf3c55ad05f56c32e7b96dcca0830a2a72348"},
{file = "ruff-0.0.285-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2933cc9631f453305399c7b8fb72b113ad76b49ae1d7103cc4afd3a423bed164"},
{file = "ruff-0.0.285-py3-none-win32.whl", hash = "sha256:770c5eb6376de024111443022cda534fb28980a9dd3b4abc83992a8770167ba6"},
{file = "ruff-0.0.285-py3-none-win_amd64.whl", hash = "sha256:a8c6ad6b9cd77489bf6d1510950cbbe47a843aa234adff0960bae64bd06c3b6d"},
{file = "ruff-0.0.285-py3-none-win_arm64.whl", hash = "sha256:de44fbc6c3b25fccee473ddf851416fd4e246fc6027b2197c395b1b3b3897921"},
{file = "ruff-0.0.285.tar.gz", hash = "sha256:45866048d1dcdcc80855998cb26c4b2b05881f9e043d2e3bfe1aa36d9a2e8f28"},
{file = "ruff-0.0.286-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:8e22cb557e7395893490e7f9cfea1073d19a5b1dd337f44fd81359b2767da4e9"},
{file = "ruff-0.0.286-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:68ed8c99c883ae79a9133cb1a86d7130feee0397fdf5ba385abf2d53e178d3fa"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8301f0bb4ec1a5b29cfaf15b83565136c47abefb771603241af9d6038f8981e8"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acc4598f810bbc465ce0ed84417ac687e392c993a84c7eaf3abf97638701c1ec"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88c8e358b445eb66d47164fa38541cfcc267847d1e7a92dd186dddb1a0a9a17f"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0433683d0c5dbcf6162a4beb2356e820a593243f1fa714072fec15e2e4f4c939"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddb61a0c4454cbe4623f4a07fef03c5ae921fe04fede8d15c6e36703c0a73b07"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47549c7c0be24c8ae9f2bce6f1c49fbafea83bca80142d118306f08ec7414041"},
{file = "ruff-0.0.286-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:559aa793149ac23dc4310f94f2c83209eedb16908a0343663be19bec42233d25"},
{file = "ruff-0.0.286-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d73cfb1c3352e7aa0ce6fb2321f36fa1d4a2c48d2ceac694cb03611ddf0e4db6"},
{file = "ruff-0.0.286-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3dad93b1f973c6d1db4b6a5da8690c5625a3fa32bdf38e543a6936e634b83dc3"},
{file = "ruff-0.0.286-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26afc0851f4fc3738afcf30f5f8b8612a31ac3455cb76e611deea80f5c0bf3ce"},
{file = "ruff-0.0.286-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9b6b116d1c4000de1b9bf027131dbc3b8a70507788f794c6b09509d28952c512"},
{file = "ruff-0.0.286-py3-none-win32.whl", hash = "sha256:556e965ac07c1e8c1c2d759ac512e526ecff62c00fde1a046acb088d3cbc1a6c"},
{file = "ruff-0.0.286-py3-none-win_amd64.whl", hash = "sha256:5d295c758961376c84aaa92d16e643d110be32add7465e197bfdaec5a431a107"},
{file = "ruff-0.0.286-py3-none-win_arm64.whl", hash = "sha256:1d6142d53ab7f164204b3133d053c4958d4d11ec3a39abf23a40b13b0784e3f0"},
{file = "ruff-0.0.286.tar.gz", hash = "sha256:f1e9d169cce81a384a26ee5bb8c919fe9ae88255f39a1a69fd1ebab233a85ed2"},
]
[[package]]
@ -3552,22 +3648,22 @@ files = [
[[package]]
name = "tornado"
version = "6.3.2"
version = "6.3.3"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = false
python-versions = ">= 3.8"
files = [
{file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"},
{file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"},
{file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"},
{file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"},
{file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"},
{file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"},
{file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"},
{file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"},
{file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"},
{file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"},
{file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"},
{file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"},
{file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"},
{file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"},
{file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"},
{file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"},
]
[[package]]
@ -3607,13 +3703,13 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "8.5.0"
version = "8.7.0"
description = "Twilio API client and TwiML generator"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-8.5.0-py2.py3-none-any.whl", hash = "sha256:a6fdea2252cb7a8a47b5750d58abe1888bba9777482bac8e9bc3be47970facc7"},
{file = "twilio-8.5.0.tar.gz", hash = "sha256:f55da9b485f9070aef09836e56230d0e6fd83811d2e6668f20d9057dd3668143"},
{file = "twilio-8.7.0-py2.py3-none-any.whl", hash = "sha256:0e8db896c8a2adefa0c1f8e725443e0da928db1de02a40687782e5f704738f98"},
{file = "twilio-8.7.0.tar.gz", hash = "sha256:ffc38ccf05cffe050670f211e872c5d8bfcad420f2ea3dcb361cb42e228b27fa"},
]
[package.dependencies]
@ -4347,4 +4443,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "5a15115f5ec615e5d2e34924b676df3efc4e8de83f8cdedb7eab055d41338121"
content-hash = "8604e4dac9b0dcc55daccab83d4c182981d21201ef9901cf9c1acdc24288f979"

View file

@ -8,7 +8,7 @@ WORKDIR /static
RUN npm ci --include=dev && npm run build-proxy
# Stage 2: Build
FROM docker.io/golang:1.21.0-bullseye AS builder
FROM docker.io/golang:1.21.0-bookworm AS builder
WORKDIR /go/src/goauthentik.io

View file

@ -152,7 +152,7 @@ packaging = "*"
paramiko = "*"
psycopg = { extras = ["c"], version = "*" }
pycryptodome = "*"
pydantic = "<2.0.0"
pydantic = "<3.0.0"
pydantic-scim = "^0.0.7"
pyjwt = "*"
python = "^3.11"

View file

@ -1,5 +1,5 @@
# Stage 1: Build
FROM docker.io/golang:1.21.0-bullseye AS builder
FROM docker.io/golang:1.21.0-bookworm AS builder
WORKDIR /go/src/goauthentik.io

View file

@ -31747,7 +31747,7 @@ components:
type: string
format: date-time
readOnly: true
users:
internal_users:
type: integer
readOnly: true
external_users:
@ -31756,27 +31756,27 @@ components:
required:
- expiry
- external_users
- internal_users
- key
- license_uuid
- name
- users
LicenseForecast:
type: object
description: Serializer for license forecast
properties:
users:
internal_users:
type: integer
external_users:
type: integer
forecasted_users:
forecasted_internal_users:
type: integer
forecasted_external_users:
type: integer
required:
- external_users
- forecasted_external_users
- forecasted_users
- users
- forecasted_internal_users
- internal_users
LicenseRequest:
type: object
description: License Serializer
@ -31790,7 +31790,7 @@ components:
type: object
description: Serializer for license status
properties:
users:
internal_users:
type: integer
external_users:
type: integer
@ -31810,11 +31810,11 @@ components:
required:
- external_users
- has_license
- internal_users
- latest_valid
- read_only
- show_admin_warning
- show_user_warning
- users
- valid
Link:
type: object

View file

@ -16,9 +16,6 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
},
"blueprints_dir": "./blueprints",
"web": {
"outpost_port_offset": 100,
},
"cert_discovery_dir": "./certs",
"geoip": "tests/GeoLite2-City-Test.mmdb",
},

View file

@ -16,6 +16,7 @@
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "lit", "custom-elements"],
"ignorePatterns": ["authentik-live-tests/**"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],

595
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,8 @@
"build-locales": "run-s build-locales:build",
"build-locales:build": "lit-localize build",
"build-locales:repair": "prettier --write ./src/locale-codes.ts",
"rollup:build": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.config.js",
"rollup:build-proxy": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.proxy.js",
"rollup:build": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.config.mjs",
"rollup:build-proxy": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.proxy.mjs",
"rollup:watch": "node --max-old-space-size=8192 node_modules/.bin/rollup -c -w",
"build": "run-s build-locales rollup:build",
"build-proxy": "run-s build-locales rollup:build-proxy",
@ -27,24 +27,24 @@
},
"dependencies": {
"@codemirror/lang-html": "^6.4.5",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-javascript": "^6.2.0",
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.4.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"@goauthentik/api": "^2023.6.1-1692308915",
"@goauthentik/api": "^2023.6.1-1692789666",
"@lit-labs/context": "^0.4.0",
"@lit-labs/task": "^3.0.1",
"@lit/localize": "^0.11.4",
"@patternfly/elements": "^2.3.2",
"@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.64.0",
"@sentry/tracing": "^7.64.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.3.3",
"chart.js": "^4.4.0",
"chartjs-adapter-moment": "^1.0.1",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
@ -52,30 +52,31 @@
"country-flag-icons": "^1.5.7",
"fuse.js": "^6.6.2",
"lit": "^2.8.0",
"mermaid": "^10.3.1",
"mermaid": "^10.4.0",
"rapidoc": "^9.3.4",
"style-mod": "^4.0.3",
"style-mod": "^4.1.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.3.1"
},
"devDependencies": {
"@babel/core": "^7.22.10",
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.22.10",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-runtime": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/preset-typescript": "^7.22.5",
"@babel/preset-typescript": "^7.22.11",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
"@lit/localize-tools": "^0.6.9",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-node-resolve": "^15.2.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.2",
"@rollup/plugin-typescript": "^11.1.3",
"@rollup/plugin-terser": "^0.4.3",
"@storybook/addon-essentials": "^7.3.2",
"@storybook/addon-links": "^7.3.2",
"@storybook/blocks": "^7.1.1",
@ -83,13 +84,13 @@
"@storybook/web-components-vite": "^7.3.2",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.8",
"@types/codemirror": "5.60.9",
"@types/grecaptcha": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tsconfig-paths": "^1.0.3",
"eslint": "^8.47.0",
"eslint": "^8.48.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.9.1",
@ -97,21 +98,19 @@
"lit-analyzer": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.2",
"pyright": "^1.1.323",
"pyright": "^1.1.324",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^2.79.1",
"rollup": "^3.28.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.3",
"rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-postcss-lit": "^2.1.0",
"rollup-plugin-terser": "^7.0.2",
"storybook": "^7.3.2",
"storybook-addon-mock": "^4.2.1",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.6.2",
"turnstile-types": "^1.1.2",
"typescript": "^5.1.6",
"typescript": "^5.2.2",
"vite-tsconfig-paths": "^4.2.0"
},
"optionalDependencies": {

View file

@ -3,10 +3,10 @@ import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import terser from "@rollup/plugin-terser";
import { cwd } from "process";
import copy from "rollup-plugin-copy";
import cssimport from "rollup-plugin-cssimport";
import { terser } from "rollup-plugin-terser";
// https://github.com/d3/d3-interpolate/issues/58
const IGNORED_WARNINGS = /Circular dependency(.*d3-[interpolate|selection])|(.*@lit\/localize.*)/;

View file

@ -1,3 +0,0 @@
import { POLY, standalone } from "./rollup.config";
export default [POLY, ...standalone];

3
web/rollup.proxy.mjs Normal file
View file

@ -0,0 +1,3 @@
import { POLY, standalone } from "./rollup.config.mjs";
export default [POLY, ...standalone];

View file

@ -12,7 +12,6 @@ import { AdminApi, Version } from "@goauthentik/api";
@customElement("ak-admin-status-version")
export class VersionStatusCard extends AdminStatusCard<Version> {
headerLink = "https://goauthentik.io/docs/releases";
icon = "pf-icon pf-icon-bundle";
getPrimaryValue(): Promise<Version> {
@ -43,16 +42,12 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
}
renderValue(): TemplateResult {
let text = this.value?.versionCurrent;
let link = `https://goauthentik.io/docs/releases/${this.value?.versionCurrent}`;
if (this.value?.buildHash) {
return html`
<a
href="https://github.com/goauthentik/authentik/commit/${this.value.buildHash}"
target="_blank"
>
${this.value.buildHash?.substring(0, 7)}
</a>
`;
text = this.value.buildHash?.substring(0, 7);
link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`;
}
return html`${this.value?.versionCurrent}`;
return html`<a href=${link} target="_blank">${text}</a>`;
}
}

View file

@ -170,11 +170,11 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
icon="pf-icon pf-icon-user"
header=${msg("Forecast internal users")}
subtext=${msg(
str`Estimated user count one year from now based on ${this.forecast?.users} current internal users and ${this.forecast?.forecastedUsers} forecasted internal users.`,
str`Estimated user count one year from now based on ${this.forecast?.internalUsers} current internal users and ${this.forecast?.forecastedInternalUsers} forecasted internal users.`,
)}
>
~&nbsp;${(this.forecast?.users || 0) +
(this.forecast?.forecastedUsers || 0)}
~&nbsp;${(this.forecast?.internalUsers || 0) +
(this.forecast?.forecastedInternalUsers || 0)}
</ak-aggregate-card>
<ak-aggregate-card
class="pf-l-grid__item"
@ -217,10 +217,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
}
return [
html`<div>${item.name}</div>`,
html`<div>
<small>0 / ${item.users}</small>
<small>0 / ${item.externalUsers}</small>
</div>`,
html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
<div>${msg(str`External: ${item.externalUsers}`)}</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>

View file

@ -246,8 +246,8 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
</ak-textarea-input>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
name="certificate"
certificate=${ifDefined(this.instance?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>

View file

@ -8,16 +8,21 @@ import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { CoreApi, CoreUsersListRequest, IntentEnum, Token, User } from "@goauthentik/api";
@customElement("ak-token-form")
export class TokenForm extends ModelForm<Token, string> {
loadInstance(pk: string): Promise<Token> {
return new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({
@state()
showExpiry = true;
async loadInstance(pk: string): Promise<Token> {
const token = await new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({
identifier: pk,
});
this.showExpiry = token.expiring || true;
return token;
}
getSuccessMessage(): string {
@ -41,6 +46,17 @@ export class TokenForm extends ModelForm<Token, string> {
}
}
renderExpiry(): TemplateResult {
return html`<ak-form-element-horizontal label=${msg("Expires on")} name="expires">
<input
type="datetime-local"
data-type="datetime-local"
value="${dateTimeLocal(first(this.instance?.expires, new Date()))}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
@ -117,6 +133,10 @@ export class TokenForm extends ModelForm<Token, string> {
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.expiring, true)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showExpiry = el.checked;
}}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -131,14 +151,7 @@ export class TokenForm extends ModelForm<Token, string> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Expires on")} name="expires">
<input
type="datetime-local"
data-type="datetime-local"
value="${dateTimeLocal(first(this.instance?.expires, new Date()))}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
${this.showExpiry ? this.renderExpiry() : html``}
</form>`;
}
}

View file

@ -17,6 +17,10 @@ import { CoreApi, User } from "@goauthentik/api";
@customElement("ak-user-form")
export class UserForm extends ModelForm<User, number> {
static get defaultUserAttributes(): { [key: string]: unknown } {
return {};
}
static get styles(): CSSResult[] {
return super.styles.concat(css`
.pf-c-button.pf-m-control {
@ -43,6 +47,9 @@ export class UserForm extends ModelForm<User, number> {
}
async send(data: User): Promise<User> {
if (data.attributes === null) {
data.attributes = UserForm.defaultUserAttributes;
}
if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
id: this.instance.pk,
@ -145,12 +152,14 @@ export class UserForm extends ModelForm<User, number> {
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Attributes")}
?required=${true}
?required=${false}
name="attributes"
>
<ak-codemirror
mode="yaml"
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(
first(this.instance?.attributes, UserForm.defaultUserAttributes),
)}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View file

@ -63,7 +63,7 @@ export function getMetaContent(key: string): string {
}
export const DEFAULT_CONFIG = new Configuration({
basePath: process.env.AK_API_BASE_PATH + "/api/v3",
basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3",
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},

View file

@ -25,8 +25,13 @@ export class LoggingMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> {
let msg = `authentik/api[${this.tenant.matchedDomain}]: `;
msg += `${context.response.status} ${context.init.method} ${context.url}`;
console.debug(msg);
// https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output
msg += `%c${context.response.status}%c ${context.init.method} ${context.url}`;
let style = "";
if (context.response.status >= 400) {
style = "color: red; font-weight: bold;";
}
console.debug(msg, style, "");
return Promise.resolve(context.response);
}
}

View file

@ -23,7 +23,7 @@ import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Pagination } from "@goauthentik/api";
import { Pagination, ResponseError } from "@goauthentik/api";
export class TableColumn {
title: string;
@ -260,7 +260,9 @@ export abstract class Table<T> extends AKElement {
renderError(): TemplateResult {
return html`<ak-empty-state header="${msg("Failed to fetch objects.")}" icon="fa-times">
<div slot="body">${this.hasError?.toString()}</div>
${this.hasError instanceof ResponseError
? html` <div slot="body">${this.hasError.message}</div> `
: html`<div slot="body">${this.hasError?.toString()}</div>`}
</ak-empty-state>`;
}
@ -268,8 +270,8 @@ export abstract class Table<T> extends AKElement {
if (this.hasError) {
return [this.renderEmpty(this.renderError())];
}
if (!this.data) {
return;
if (!this.data || this.isLoading) {
return [this.renderLoading()];
}
if (this.data.pagination.count === 0) {
return [this.renderEmpty()];
@ -499,7 +501,7 @@ export abstract class Table<T> extends AKElement {
${this.columns().map((col) => col.render(this))}
</tr>
</thead>
${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
${this.renderRows()}
</table>
${this.paginated
? html` <div class="pf-c-pagination pf-m-bottom">

View file

@ -67,6 +67,13 @@ export class LibraryApplication extends AKElement {
return html`<ak-spinner></ak-spinner>`;
}
const me = rootInterface<UserInterface>()?.me;
let expandable = false;
if (rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser) {
expandable = true;
}
if (this.application.metaPublisher !== "" || this.application.metaDescription !== "") {
expandable = true;
}
return html` <div
class="pf-c-card pf-m-hoverable pf-m-compact ${this.selected
? "pf-m-selectable pf-m-selected"
@ -89,22 +96,25 @@ export class LibraryApplication extends AKElement {
>
</div>
<div class="expander"></div>
<ak-expand textOpen=${msg("Less details")} textClosed=${msg("More details")}>
<div class="pf-c-content">
<small>${this.application.metaPublisher}</small>
</div>
${truncateWords(this.application.metaDescription || "", 10)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser
? html`
<a
class="pf-c-button pf-m-control pf-m-small pf-m-block"
href="/if/admin/#/core/applications/${this.application?.slug}"
>
<i class="fas fa-edit"></i>&nbsp;${msg("Edit")}
</a>
`
: html``}
</ak-expand>
${expandable
? html`<ak-expand textOpen=${msg("Less details")} textClosed=${msg("More details")}>
<div class="pf-c-content">
<small>${this.application.metaPublisher}</small>
</div>
${truncateWords(this.application.metaDescription || "", 10)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit &&
me?.user.isSuperuser
? html`
<a
class="pf-c-button pf-m-control pf-m-small pf-m-block"
href="/if/admin/#/core/applications/${this.application?.slug}"
>
<i class="fas fa-edit"></i>&nbsp;${msg("Edit")}
</a>
`
: html``}
</ak-expand>`
: html``}
</div>`;
}
}

View file

@ -116,43 +116,63 @@ export class UserSettingsPage extends AKElement {
data-tab-title="${msg("Sessions")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-session-list
targetUser=${ifDefined(
rootInterface<UserInterface>()?.me?.user.username,
)}
></ak-user-session-list>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-session-list
targetUser=${ifDefined(
rootInterface<UserInterface>()?.me?.user.username,
)}
></ak-user-session-list>
</div>
</div>
</section>
<section
slot="page-consents"
data-tab-title="${msg("Consent")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-consent-list
userId=${ifDefined(rootInterface<UserInterface>()?.me?.user.pk)}
></ak-user-consent-list>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-consent-list
userId=${ifDefined(rootInterface<UserInterface>()?.me?.user.pk)}
></ak-user-consent-list>
</div>
</div>
</section>
<section
slot="page-mfa"
data-tab-title="${msg("MFA Devices")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-settings-mfa
.userSettings=${this.userSettings}
></ak-user-settings-mfa>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-settings-mfa
.userSettings=${this.userSettings}
></ak-user-settings-mfa>
</div>
</div>
</section>
<section
slot="page-sources"
data-tab-title="${msg("Connected services")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-settings-source></ak-user-settings-source>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-settings-source></ak-user-settings-source>
</div>
</div>
</section>
<section
slot="page-tokens"
data-tab-title="${msg("Tokens and App passwords")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-token-list></ak-user-token-list>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-token-list></ak-user-token-list>
</div>
</div>
</section>
</ak-tabs>
</main>

View file

@ -5806,7 +5806,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5888,6 +5888,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -6122,7 +6122,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6204,6 +6204,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -5714,7 +5714,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5796,6 +5796,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -5821,7 +5821,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5903,6 +5903,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

7726
web/xliff/nl.xlf Normal file

File diff suppressed because it is too large Load diff

View file

@ -5953,7 +5953,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6035,6 +6035,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -6057,7 +6057,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6139,6 +6139,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -5704,7 +5704,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5786,6 +5786,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -1,4 +1,4 @@
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@ -618,9 +618,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>"。</target>
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@ -1072,8 +1072,8 @@
</trans-unit>
<trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="s55787f4dfcdce52b">
@ -1819,8 +1819,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@ -3248,8 +3248,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit>
<trans-unit id="s026555347e589f0e">
@ -4046,8 +4046,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target>
</trans-unit>
<trans-unit id="s44536d20bb5c8257">
@ -4056,8 +4056,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s3bb51cabb02b997e">
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
<source>Format: &quot;weeks=3;days=2;hours=3,seconds=2&quot;.</source>
<target>格式:&quot;weeks=3;days=2;hours=3,seconds=2&quot;。</target>
</trans-unit>
<trans-unit id="s04bfd02201db5ab8">
@ -4253,10 +4253,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@ -5372,7 +5372,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A "roaming" authenticator, like a YubiKey</source>
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@ -5712,10 +5712,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target>
<x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
<x id="0" equiv-text="${prompt.name}"/>&quot;
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为
<x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit>
@ -5764,7 +5764,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@ -7658,8 +7658,8 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>预测内部用户</target>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.internalUsers}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -7759,13 +7759,24 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s7caa8f7edb920909">
<source>The number of tokens generated whenever this stage is used. Every token generated per stage execution will be attached to a single static device.</source>
<target>使用此阶段时生成的令牌数量。每次阶段执行中生成的每个令牌都会被附加到单个静态设备上。</target>
</trans-unit>
<trans-unit id="s4aacc4e0277c1042">
<source>Token length</source>
<target>令牌长度</target>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
<target>每个生成令牌的长度。可以增加以增强安全性。</target>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
<target>内部:<x id="0" equiv-text="${item.internalUsers}"/></target>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
<target>外部:<x id="0" equiv-text="${item.externalUsers}"/></target>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View file

@ -5759,7 +5759,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5841,6 +5841,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -7658,8 +7658,8 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>预测内部用户</target>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.internalUsers}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -7768,6 +7768,14 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
<target>每个生成令牌的长度。可以增加以增强安全性。</target>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
<target>内部:<x id="0" equiv-text="${item.internalUsers}"/></target>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
<target>外部:<x id="0" equiv-text="${item.externalUsers}"/></target>
</trans-unit>
</body>
</file>

View file

@ -5758,7 +5758,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5840,6 +5840,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View file

@ -28,7 +28,7 @@ The danger with letting identity become boring is that were not engaging in t
> My pitch: Lets make identity fun again. And in doing so, lets think through a better way to decide whether to build or buy software.
!["Image by <a href="https://pixabay.com/users/jplenio-7645255/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3092026">Joe</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3092026">Pixabay</a>"](./image1.jpg)
[![Image1](./image1.jpg)](https://pixabay.com/users/jplenio-7645255/ "Image by jplenio on pixabay")
<!--truncate-->

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -0,0 +1,67 @@
---
title: "My hobby became my job, 50% extra pay, just needed to let go of GPLv3"
slug: 2023-08-23-my-hobby-became-my-job
authors:
- name: Jens Langhammer
title: CTO at Authentik Security Inc
url: https://github.com/BeryJu
image_url: https://github.com/BeryJu.png
tags:
- founder
- SSO
- open source
- identity provider
- licensing
- gpl
- mit
- security
- authentication
hide_table_of_contents: false
image: ./image1.jpg
---
Theres been a lot of discussion about licensing in the news, with [Red Hat](https://www.redhat.com/en/blog/furthering-evolution-centos-stream) and now [Hashicorp](https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license) notably adjusting their licensing models to be more “business friendly,” and [Codecov](https://blog.sentry.io/lets-talk-about-open-source/) (proudly, and mistakenly) [pronouncing](https://about.codecov.io/blog/codecov-is-now-open-source/) they are now “open source.”
“Like the rest of them, they have redefined Open as in Open for business”—[jquast on Hacker News](https://news.ycombinator.com/item?id=37021360)
This is a common tension when youre building commercially on top of open source, so I wanted to share some reflections from my own experience of going from MIT, to GPL, back to MIT.
!["Photo by <a href="https://unsplash.com/@gcalebjones?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Caleb Jones</a> on <a href="https://unsplash.com/photos/J3JMyXWQHXU?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>"](./image1.jpg)
<!--truncate-->
I started working on the project that led to [authentik](https://github.com/goauthentik/authentik) when I was 20. My original vision was a single pane of glass for emails, domains, applications, hosting, and so on. This was overly ambitious for one person and their hobby project, and I ended up spending most of my time on the SSO part. This became its own project: Passbook (later [renamed to authentik](https://github.com/goauthentik/authentik/pull/361) due to a [naming conflict](https://techcrunch.com/2015/06/08/apple-rebrands-passbook-to-wallet/)).
Initially, authentik used the MIT license. When [Elastic called out AWS](https://www.elastic.co/blog/why-license-change-aws) for trademark abuse (offering Elasticsearch as an AWS service without collaborating with Elastic), I [changed it to GPLv3](https://github.com/goauthentik/authentik/commit/4671d4afb4d32988ca0058a33888862bd9652b16) because I didnt like what AWS did in principle, and didnt want it to happen to authentik.
# An opportunity, and a compromise
Two years later, [Sid](https://www.linkedin.com/in/sijbrandij/) at [Open Core Ventures](https://opencoreventures.com/) (OCV) contacted me about [creating a company](../2022-11-02-the-next-step-for-authentik/item.md), building on the features and functionality of authentik. It was a dream opportunity: work full time on my hobby project and make 25% more in the process. But I had to let go of the GPL license.
With an open core model customers are usually using code from both the open source and proprietary codebases. This necessitates a dual license structure, meaning customers need to accept both licenses.
The drawback of building commercially on top of open source software using GPL is that the copyleft aspect can put some people off. Not every person or business wants to have to expose their code for every minor change or bug fix they may add, and they will sooner find a competitor with a more permissive license than adopt your software. This is obviously not ideal when youre trying to get traction and grow a business.
OCV proposed we switch back to MIT.
# Considerations and tradeoffs
I was very conflicted about reverting to MIT because we had chosen GPL for a reason, but the circumstances had changed. As a company and a real legal entity, we would have recourse if something like AWS/Elasticsearch were to happen—it wouldnt just be me trying to defend myself while also doing my day job. The decision forced me to reflect on what it means to build a company on top of an existing open source project.
For me, it was an opportunity to work full time on a passion project, with more resources to invest in building and maintaining the open core of the project. The opportunity came with tradeoffs to be made, and a responsibility to be a good steward of the open source project.
I know how volatile startups can be. I had put so much time into authentik already, and my biggest concern was around what happens if things dont work out. I wanted to make sure that the open source version stays free, vibrant, and open for use by all.
## A license isnt the only way to guarantee good behavior
With a permissive license, the risk of [bait and switch](https://opencoreventures.com/blog/2022-10-preventing-the-bait-and-switch-open-core/) is always there. A commercial company needs to become profitable and there is precedent for changing to more limited licenses when it suits the business. People naturally see this as a dichotomy: you either have a copyleft license and therefore your intentions are enshrined in the license, or a permissive one and cant be trusted to uphold open source ideals.
There is a third path though, which is the route we eventually took with [Authentik Security](https://goauthentik.io/), the company we were building on top of the project. We incorporated as a public benefit company, which means that we are legally bound by the terms in the [OCV Public Benefit Company Charter](https://github.com/OpenCoreVentures/ocv-public-benefit-company/blob/main/ocv-public-benefit-company-charter.md). This includes commitments to keeping open source products open source, and ensuring the majority of new features added in a calendar year are made available under an open source license. Being a public benefit company means we are still held accountable, just through a different mechanism than the license.
# The process of changing the license
Changing licenses is a sensitive issue. I consulted with the top contributors to authentik to hear their feedback while we were in the process of setting up Authentik Security. Nobody objected, so we [switched back to MIT](https://github.com/goauthentik/authentik/commit/47132faffbac1098dadba73435164e655901e9e7) and announced the change in the [company announcement post](https://goauthentik.io/blog/2022-11-02-the-next-step-for-authentik). I think I was surprised there wasnt a backlash or accusations of putting profit over principle (we have all seen [how](https://news.ycombinator.com/item?id=37081306) [impassioned](https://news.ycombinator.com/item?id=36971490) [people](https://news.ycombinator.com/item?id=37003489) [get](https://news.ycombinator.com/item?id=36990036) about open source and ideals). I like to think that people saw the pragmatism in the decision: that MIT lets us further the work of authentik.
# Reflections
While a copyleft license is one way to hold companies accountable to upholding the principles of open source, with Authentik Security we struck a balance between commercial viability with the more permissive MIT license and the values I wanted to entrench with becoming a Public Benefit Company. I now get to work full time on my hobby, and the core of authentik is still open source.

View file

@ -124,7 +124,7 @@ If you have any questions or comments about this advisory:
Subject: `Notice of upcoming authentik Security releases 2022.10.3 and 2022.11.3`
```markdown
We'll be publishing a security Issue (CVE-2022-xxxxx) and accompanying fix on _date_, 13:00 UTC with the Criticality level High. Fixed versions x, y and z will be released alongside a workaround for previous versions. For more info, see the authentik Security policy here: https://goauthentik.io/docs/security/policy.
We'll be publishing a security Issue (CVE-2022-xxxxx) and accompanying fix on _date_, 13:00 UTC with the Severity level High. Fixed versions x, y and z will be released alongside a workaround for previous versions. For more info, see the authentik Security policy here: https://goauthentik.io/docs/security/policy.
```
</p>
@ -134,7 +134,7 @@ We'll be publishing a security Issue (CVE-2022-xxxxx) and accompanying fix on _d
<p>
```markdown
@everyone We'll be publishing a security Issue (CVE-2022-xxxxx) and accompanying fix on _date_, 13:00 UTC with the Criticality level High. Fixed versions x, y and z will be released alongside a workaround for previous versions. For more info, see the authentik Security policy here: https://goauthentik.io/docs/security/policy.
@everyone We'll be publishing a security Issue (CVE-2022-xxxxx) and accompanying fix on _date_, 13:00 UTC with the Severity level High. Fixed versions x, y and z will be released alongside a workaround for previous versions. For more info, see the authentik Security policy here: https://goauthentik.io/docs/security/policy.
```
</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View file

@ -1,57 +0,0 @@
---
title: Release 2023.7
slug: "/releases/2023.7"
---
## Breaking changes
- Removal of PostgreSQL 11 support
As announced in the [2023.5](./v2023.5.md) release notes (and postponed by a release), this release requires PostgreSQL 12 or newer. This is due to a changed requirement in a framework we use, Django.
This does not affect docker-compose installations (as these already ship with PostgreSQL 12), however it is still recommended to upgrade to a newer version when convenient.
For Kubernetes install, a manual one-time migration has to be done: [Upgrading PostgreSQL on Kubernetes](../../troubleshooting/postgres/upgrade_kubernetes.md)
- Changed nested Group membership behaviour
In previous versions, nested groups were handled very inconsistently. Binding a group to an application/etc would check the membership recursively, however when using `user.ak_groups.all()` would only return direct memberships. Additionally, using `user.group_attributes()` would do the same and only merge all group attributes for direct memberships.
This has been changed to always use the same logic as when checking for access, which means dealing with complex group structures is a lot more consistent.
Policies that do use `user.ak_groups.all()` will retain the current behaviour, to use the new behaviour replace the call with `user.all_groups()`.
## New features
## Upgrading
This release does not introduce any new requirements.
### docker-compose
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
```
wget -O docker-compose.yml https://goauthentik.io/version/2023.7/docker-compose.yml
docker-compose up -d
```
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
### Kubernetes
Update your values to use the new images:
```yaml
image:
repository: ghcr.io/goauthentik/server
tag: 2023.7.0
```
## Minor changes/fixes
<!-- _Insert the output of `make gen-changelog` here_ -->
## API Changes
<!-- _Insert output of `make gen-diff` here_ -->

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,4 @@
const fs = require("fs").promises;
const sidebar = require("./sidebars.js");
const releases = sidebar.docs
.filter((doc) => doc.link?.slug === "releases")[0]
.items.filter((release) => typeof release === "string");
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = async function () {
@ -21,8 +16,8 @@ module.exports = async function () {
organizationName: "Authentik Security Inc.",
projectName: "authentik",
themeConfig: {
image: "img/social.png",
navbar: {
title: "authentik",
logo: {
alt: "authentik logo",
src: "img/icon_left_brand.svg",
@ -49,26 +44,6 @@ module.exports = async function () {
label: "Pricing",
position: "left",
},
{
type: "dropdown",
label: `Version: ${releases[0].replace(
/releases\/\d+\/v/,
"",
)}`,
position: "right",
items: releases.map((release) => {
const version = release.replace(
/releases\/\d+\/v/,
"",
);
const subdomain = version.replace(".", "-");
const label = `Version: ${version}`;
return {
label: label,
href: `https://version-${subdomain}.goauthentik.io`,
};
}),
},
{
href: "https://github.com/goauthentik/authentik",
className: "header-github-link",

View file

@ -1,5 +1,11 @@
module.exports = {
const generateVersionDropdown =
require("./src/utils.js").generateVersionDropdown;
const docsSidebar = {
docs: [
{
type: "html",
},
{
type: "doc",
id: "index",
@ -351,3 +357,6 @@ module.exports = {
},
],
};
docsSidebar.docs[0].value = generateVersionDropdown(docsSidebar);
module.exports = docsSidebar;

View file

@ -1,5 +1,13 @@
const docsSidebar = require("./sidebars.js");
const generateVersionDropdown =
require("./src/utils.js").generateVersionDropdown;
module.exports = {
docs: [
{
type: "html",
value: generateVersionDropdown(docsSidebar),
},
{
type: "doc",
id: "index",

View file

@ -1,5 +1,13 @@
const docsSidebar = require("./sidebars.js");
const generateVersionDropdown =
require("./src/utils.js").generateVersionDropdown;
module.exports = {
integrations: [
{
type: "html",
value: generateVersionDropdown(docsSidebar),
},
{
type: "category",
label: "Applications",

View file

@ -15,7 +15,7 @@ export default function Comparison() {
"Requires additional product: Web Application Proxy";
return (
<>
<a id="comparison"></a>
<div id="comparison"></div>
<h1 className="header">Why authentik?</h1>
<div className="table-responsive">
<div className="table">

View file

@ -26,18 +26,15 @@
box-shadow: none;
}
/* Don't display text title */
.navbar__title {
display: none;
}
.navbar__logo {
margin: 0 0.75rem;
}
/* Match color of light/dark theme switch */
.navbar__items--right svg {
.navbar__items--right svg,
.navbar-sidebar__brand svg {
color: var(--white);
stroke: var(--white);
}
.hero--primary {
@ -116,3 +113,28 @@ body {
align-items: center;
justify-content: center;
}
/* styling for version selector in sidebar */
.theme-doc-sidebar-menu .dropdown {
display: block;
padding: 0;
}
.theme-doc-sidebar-menu .navbar__link {
color: var(--ifm-menu-color);
}
.theme-doc-sidebar-menu .dropdown__menu {
left: 0;
}
.theme-doc-sidebar-menu hr {
margin-top: calc(var(--ifm-hr-margin-vertical) / 2);
margin-right: -0.5rem;
}
/* Nav header background color on mobile */
.navbar-sidebar__brand,
.navbar-sidebar__items {
background-color: var(--ifm-color-primary);
}
.navbar-sidebar__items .menu__link {
color: var(--white);
}

31
website/src/utils.js Normal file
View file

@ -0,0 +1,31 @@
function generateVersionDropdown(sidebar) {
const releases = sidebar.docs
.filter((doc) => doc.link?.slug === "releases")[0]
.items.filter((release) => typeof release === "string");
const latest = releases[0].replace(/releases\/\d+\/v/, "");
return `<div class="navbar__item dropdown dropdown--hoverable dropdown--right">
<div aria-haspopup="true" aria-expanded="false" role="button" class="navbar__link menu__link">
Version: ${latest}
</div>
<ul class="dropdown__menu">
${releases
.map((release) => {
const version = release.replace(/releases\/\d+\/v/, "");
const subdomain = `version-${version.replace(".", "-")}`;
const label = `Version: ${version}`;
return `<li>
<a
href="https://${subdomain}.goauthentik.io/docs"
target="_blank" rel="noopener noreferrer"
class="dropdown__link">${label}</a>
</li>`;
})
.join("")}
</ul>
</div>
<hr>`;
}
module.exports = {
generateVersionDropdown,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -6,6 +6,9 @@ import glob from "glob";
const getSidebarItems = () => {
const allItems = [];
const mapper = (category) => {
if (!category.items) {
return;
}
category.items.forEach((item) => {
if (item.constructor === String) {
allItems.push(item);