mirror of
https://github.com/goauthentik/authentik
synced 2026-04-26 01:25:02 +02:00
Compare commits
107 Commits
form-file-
...
admin/vers
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbce9611d2 | ||
|
|
e6643a69cd | ||
|
|
0fdeaee559 | ||
|
|
f9fd1bbf09 | ||
|
|
3ba3b11a76 | ||
|
|
19e558e916 | ||
|
|
e15fadfedd | ||
|
|
52854e61c7 | ||
|
|
53aa0113ca | ||
|
|
9f71face62 | ||
|
|
2fadefb5b4 | ||
|
|
23e92bceae | ||
|
|
1ff2eea20a | ||
|
|
abcd2179bf | ||
|
|
6a4b5850a0 | ||
|
|
821c8c36cd | ||
|
|
8838efe3c0 | ||
|
|
433a4a3037 | ||
|
|
2d69a67e9d | ||
|
|
1294cc64e8 | ||
|
|
910326a05a | ||
|
|
9257b3e570 | ||
|
|
cdd18a7e5a | ||
|
|
88bea46648 | ||
|
|
295090a80b | ||
|
|
bff607a5c3 | ||
|
|
bfb2fb4fcf | ||
|
|
93015b0fce | ||
|
|
9b6c0d3f1a | ||
|
|
66e95ddb20 | ||
|
|
c5d8524a7d | ||
|
|
a4761064c2 | ||
|
|
b0de8bf71f | ||
|
|
32100fd3b9 | ||
|
|
4815e97162 | ||
|
|
dee99c38bb | ||
|
|
a024056b62 | ||
|
|
a8dc21b707 | ||
|
|
7ccda743df | ||
|
|
0c795dd077 | ||
|
|
5df9ed3582 | ||
|
|
a47b4934a5 | ||
|
|
338a6e74f4 | ||
|
|
8897af1048 | ||
|
|
56ec3f7def | ||
|
|
53fd893d91 | ||
|
|
f7d9a8cafe | ||
|
|
f97c1071f3 | ||
|
|
4da1115a7c | ||
|
|
63b1ccd4c3 | ||
|
|
63aa7f4684 | ||
|
|
d997930b60 | ||
|
|
a088a62981 | ||
|
|
118e05f256 | ||
|
|
b30500094f | ||
|
|
21af51ba59 | ||
|
|
87da0497e0 | ||
|
|
87317d6e7f | ||
|
|
071305da18 | ||
|
|
1dc8ed5e55 | ||
|
|
dc8dee985f | ||
|
|
2b20b06baa | ||
|
|
6cab1f85e4 | ||
|
|
f836c38b18 | ||
|
|
07e373e505 | ||
|
|
e361d38978 | ||
|
|
3ba1691db6 | ||
|
|
7c2987ea32 | ||
|
|
4ca88caf07 | ||
|
|
6c939341b0 | ||
|
|
4142584788 | ||
|
|
f6fbafd280 | ||
|
|
7c9555bee8 | ||
|
|
82cd64dfe7 | ||
|
|
28f0b48e33 | ||
|
|
38c02dc490 | ||
|
|
79505969db | ||
|
|
9870888456 | ||
|
|
5c06e1920e | ||
|
|
1506ad8aa4 | ||
|
|
21b6204c90 | ||
|
|
05621735cb | ||
|
|
f9ffd35ab8 | ||
|
|
c3ded3a835 | ||
|
|
7629c22050 | ||
|
|
29a66410fd | ||
|
|
f147d40c5f | ||
|
|
15b556c1be | ||
|
|
522e8a26a2 | ||
|
|
403d762f65 | ||
|
|
cbc65ffd74 | ||
|
|
9a9bafdfb4 | ||
|
|
198d2a1a8a | ||
|
|
239edace16 | ||
|
|
370d5ff0c0 | ||
|
|
635b09621b | ||
|
|
4335498ac5 | ||
|
|
72af009de8 | ||
|
|
3a07d5d829 | ||
|
|
7122891f0f | ||
|
|
c32d6cc75e | ||
|
|
eaf6be74f3 | ||
|
|
c35650afbd | ||
|
|
a1f9ff8b7d | ||
|
|
962f7513ba | ||
|
|
0ec5ea69ef | ||
|
|
d8a3098329 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -31,4 +31,4 @@ If changes to the frontend have been made
|
||||
If applicable
|
||||
|
||||
- [ ] The documentation has been updated
|
||||
- [ ] The documentation has been formatted (`make website`)
|
||||
- [ ] The documentation has been formatted (`make docs`)
|
||||
|
||||
2
.github/workflows/api-ts-publish.yml
vendored
2
.github/workflows/api-ts-publish.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
working-directory: gen-ts-api/
|
||||
run: |
|
||||
npm i
|
||||
npm publish
|
||||
npm publish --tag generated
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
- name: Upgrade /web
|
||||
|
||||
@@ -62,7 +62,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/authentik ./cmd/server
|
||||
|
||||
# Stage 3: MaxMind GeoIP
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.1 AS geoip
|
||||
|
||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||
ENV GEOIPUPDATE_VERBOSE="1"
|
||||
@@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.19 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.21 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
|
||||
43
Makefile
43
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: gen dev-reset all clean test web website
|
||||
.PHONY: gen dev-reset all clean test web docs
|
||||
|
||||
SHELL := /usr/bin/env bash
|
||||
.SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail
|
||||
@@ -73,7 +73,7 @@ core-i18n-extract:
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
install: web-install website-install core-install ## Install all requires dependencies for `web`, `website` and `core`
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
|
||||
@@ -183,18 +183,23 @@ gen-dev-config: ## Generate a local development config file
|
||||
|
||||
gen: gen-build gen-client-ts
|
||||
|
||||
#########################
|
||||
## Node.js
|
||||
#########################
|
||||
|
||||
node-install: ## Install the necessary libraries to build Node.js packages
|
||||
npm ci
|
||||
npm ci --prefix web
|
||||
|
||||
#########################
|
||||
## Web
|
||||
#########################
|
||||
|
||||
web-build: web-install ## Build the Authentik UI
|
||||
web-build: node-install ## Build the Authentik UI
|
||||
cd web && npm run build
|
||||
|
||||
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
|
||||
|
||||
web-install: ## Install the necessary libraries to build the Authentik UI
|
||||
cd web && npm ci
|
||||
|
||||
web-test: ## Run tests for the Authentik UI
|
||||
cd web && npm run test
|
||||
|
||||
@@ -221,22 +226,28 @@ web-i18n-extract:
|
||||
cd web && npm run extract-locales
|
||||
|
||||
#########################
|
||||
## Website
|
||||
## Docs
|
||||
#########################
|
||||
|
||||
website: website-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
|
||||
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
|
||||
|
||||
website-install:
|
||||
cd website && npm ci
|
||||
docs-install:
|
||||
npm ci --prefix website
|
||||
|
||||
website-lint-fix: lint-codespell
|
||||
cd website && npm run prettier
|
||||
docs-lint-fix: lint-codespell
|
||||
npm run prettier --prefix website
|
||||
|
||||
website-build:
|
||||
cd website && npm run build
|
||||
docs-build:
|
||||
npm run build --prefix website
|
||||
|
||||
website-watch: ## Build and watch the documentation website, updating automatically
|
||||
cd website && npm run watch
|
||||
docs-watch: ## Build and watch the topics documentation
|
||||
npm run start --prefix website
|
||||
|
||||
docs-integrations-build:
|
||||
npm run build --prefix website -w integrations
|
||||
|
||||
docs-integrations-watch: ## Build and watch the Integrations documentation
|
||||
npm run start --prefix website -w integrations
|
||||
|
||||
#########################
|
||||
## Docker
|
||||
|
||||
@@ -42,7 +42,11 @@ class Exporter:
|
||||
if model in self.excluded_models:
|
||||
continue
|
||||
for obj in self.get_model_instances(model):
|
||||
yield BlueprintEntry.from_model(obj)
|
||||
yield BlueprintEntry.from_model(self.alter_model(obj))
|
||||
|
||||
def alter_model(self, model: Model):
|
||||
"""Hook to modify the model before exporting"""
|
||||
return model
|
||||
|
||||
def get_model_instances(self, model: type[Model]) -> QuerySet:
|
||||
"""Return a queryset for `model`. Can be used to filter some
|
||||
|
||||
@@ -11,7 +11,6 @@ from authentik.core.expression.exceptions import SkipObjectException
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.types import PolicyRequest
|
||||
|
||||
PROPERTY_MAPPING_TIME = Histogram(
|
||||
@@ -69,12 +68,11 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
# For dry-run requests we don't save exceptions
|
||||
if self.dry_run:
|
||||
return
|
||||
error_string = exception_to_string(exc)
|
||||
event = Event.new(
|
||||
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
||||
expression=expression_source,
|
||||
message=error_string,
|
||||
)
|
||||
message="Failed to execute property mapping",
|
||||
).with_exception(exc)
|
||||
if "request" in self._context:
|
||||
req: PolicyRequest = self._context["request"]
|
||||
if req.http_request:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -10,16 +11,21 @@ from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.enterprise.bundle import generate_support_bundle
|
||||
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.permissions import HasPermission
|
||||
from authentik.tenants.utils import get_unique_identifier
|
||||
|
||||
|
||||
@@ -53,6 +59,7 @@ class LicenseSerializer(ModelSerializer):
|
||||
"external_users",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"key": {"validators": [UniqueValidator(queryset=License.objects.all())]},
|
||||
"name": {"read_only": True},
|
||||
"expiry": {"read_only": True},
|
||||
"internal_users": {"read_only": True},
|
||||
@@ -145,3 +152,24 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
response.is_valid(raise_exception=True)
|
||||
return Response(response.data)
|
||||
|
||||
|
||||
class BinaryRenderer(BaseRenderer):
|
||||
media_type = "application/gzip"
|
||||
format = "bin"
|
||||
|
||||
|
||||
class SupportBundleView(APIView):
|
||||
"""Generate a support bundle."""
|
||||
|
||||
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
renderer_classes = [BinaryRenderer]
|
||||
|
||||
@extend_schema(responses=bytes, request=None)
|
||||
def post(self, request: Request) -> Response:
|
||||
"""Generate a support bundle."""
|
||||
response = HttpResponse(generate_support_bundle(), content_type=BinaryRenderer.media_type)
|
||||
response["Content-Disposition"] = 'attachment; filename="authentik_support.tgz"'
|
||||
return response
|
||||
|
||||
@@ -65,13 +65,17 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
data[field.name] = deepcopy(field_value)
|
||||
return cleanse_dict(data)
|
||||
|
||||
def diff(self, before: dict, after: dict) -> dict:
|
||||
def diff(self, before: dict, after: dict, update_fields: list[str] | None = None) -> dict:
|
||||
"""Generate diff between dicts"""
|
||||
diff = {}
|
||||
for key, value in before.items():
|
||||
if update_fields and key not in update_fields:
|
||||
continue
|
||||
if after.get(key) != value:
|
||||
diff[key] = {"previous_value": value, "new_value": after.get(key)}
|
||||
for key, value in after.items():
|
||||
if update_fields and key not in update_fields:
|
||||
continue
|
||||
if key not in before and key not in diff and before.get(key) != value:
|
||||
diff[key] = {"previous_value": before.get(key), "new_value": value}
|
||||
return sanitize_item(diff)
|
||||
@@ -95,6 +99,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
instance: Model,
|
||||
created: bool,
|
||||
thread_kwargs: dict | None = None,
|
||||
update_fields: list[str] | None = None,
|
||||
**_,
|
||||
):
|
||||
if not self.enabled:
|
||||
@@ -108,7 +113,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
prev_state = {}
|
||||
# Get current state
|
||||
new_state = self.serialize_simple(instance)
|
||||
diff = self.diff(prev_state, new_state)
|
||||
diff = self.diff(prev_state, new_state, update_fields)
|
||||
thread_kwargs["diff"] = diff
|
||||
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.enterprise.audit.middleware import EnterpriseAuditMiddleware
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.lib.generators import generate_id
|
||||
@@ -208,3 +209,23 @@ class TestEnterpriseAudit(APITestCase):
|
||||
diff,
|
||||
{"users": {"remove": [user.pk]}},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_diff_update_fields(self):
|
||||
"""Test update audit log"""
|
||||
self.client.force_login(self.user)
|
||||
diff = EnterpriseAuditMiddleware(None).diff(
|
||||
{
|
||||
"foo": "bar",
|
||||
"is_active": False,
|
||||
},
|
||||
{
|
||||
"foo": "baz",
|
||||
"is_active": True,
|
||||
},
|
||||
update_fields=["is_active"],
|
||||
)
|
||||
self.assertEqual(diff, {"is_active": {"new_value": True, "previous_value": False}})
|
||||
|
||||
53
authentik/enterprise/bundle.py
Normal file
53
authentik/enterprise/bundle.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import re
|
||||
from io import BytesIO
|
||||
from tarfile import TarInfo, open
|
||||
|
||||
from django.db.models import Model
|
||||
from django.db.models.fields import CharField, SlugField, TextField
|
||||
from django.db.models.fields.json import JSONField
|
||||
|
||||
from authentik.blueprints.v1.exporter import Exporter
|
||||
from authentik.core.models import User
|
||||
from lifecycle.support import encrypt, generate
|
||||
|
||||
SENSITIVE_VALUE_PLACEHOLDER = "<REDACTED>"
|
||||
|
||||
|
||||
class SupportExporter(Exporter):
|
||||
"""Blueprint exporter which censors sensitive model attributes"""
|
||||
|
||||
sensitive_fields = re.compile(
|
||||
# Partially taken from Django's SafeExceptionReporterFilter
|
||||
"API|AUTH|TOKEN|KEY|SECRET|PASS|SIGNATURE|CREDENTIALS",
|
||||
re.I,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.excluded_models.append(User)
|
||||
|
||||
def alter_model(self, model: Model):
|
||||
for field in model._meta.fields:
|
||||
if not self.sensitive_fields.search(field.name):
|
||||
continue
|
||||
if isinstance(field, TextField | CharField | SlugField):
|
||||
setattr(model, field.name, SENSITIVE_VALUE_PLACEHOLDER)
|
||||
elif isinstance(field, JSONField):
|
||||
setattr(model, field.name, {})
|
||||
return model
|
||||
|
||||
|
||||
def generate_support_bundle():
|
||||
fh = BytesIO()
|
||||
exporter = SupportExporter()
|
||||
files = {
|
||||
"authentik/support.jwe": encrypt(generate()),
|
||||
"authentik/blueprint.yaml": exporter.export_to_string(),
|
||||
}
|
||||
with open(fileobj=fh, mode="w:gz") as tar:
|
||||
for path, file in files.items():
|
||||
info = TarInfo(path)
|
||||
info.size = len(file)
|
||||
tar.addfile(info, BytesIO(file.encode()))
|
||||
final_data = fh.getvalue()
|
||||
return final_data
|
||||
@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Setup Google Chrome Device-trust connection"""
|
||||
"""Setup Google Chrome Device Trust connection"""
|
||||
|
||||
credentials = models.JSONField()
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.api import LicenseViewSet
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.api import LicenseViewSet, SupportBundleView
|
||||
|
||||
api_urlpatterns = [
|
||||
("enterprise/license", LicenseViewSet),
|
||||
path(
|
||||
"enterprise/support_bundle/", SupportBundleView.as_view(), name="enterprise_support_bundle"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ from authentik.core.models import Group, User
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.lib.sentry import should_ignore_exception
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.stages.authenticator_static.models import StaticToken
|
||||
|
||||
IGNORED_MODELS = tuple(
|
||||
@@ -170,14 +170,16 @@ class AuditMiddleware:
|
||||
thread = EventNewThread(
|
||||
EventAction.SUSPICIOUS_REQUEST,
|
||||
request,
|
||||
message=exception_to_string(exception),
|
||||
message=str(exception),
|
||||
exception=exception_to_dict(exception),
|
||||
)
|
||||
thread.run()
|
||||
elif not should_ignore_exception(exception):
|
||||
thread = EventNewThread(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
request,
|
||||
message=exception_to_string(exception),
|
||||
message=str(exception),
|
||||
exception=exception_to_dict(exception),
|
||||
)
|
||||
thread.run()
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ from authentik.events.utils import (
|
||||
)
|
||||
from authentik.lib.models import DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
@@ -163,6 +164,12 @@ class Event(SerializerModel, ExpiringModel):
|
||||
event = Event(action=action, app=app, context=cleaned_kwargs)
|
||||
return event
|
||||
|
||||
def with_exception(self, exc: Exception) -> "Event":
|
||||
"""Add data from 'exc' to the event in a database-saveable format"""
|
||||
self.context.setdefault("message", str(exc))
|
||||
self.context["exception"] = exception_to_dict(exc)
|
||||
return self
|
||||
|
||||
def set_user(self, user: User) -> "Event":
|
||||
"""Set `.user` based on user, ensuring the correct attributes are copied.
|
||||
This should only be used when self.from_http is *not* used."""
|
||||
|
||||
@@ -127,8 +127,8 @@ class SystemTask(TenantTask):
|
||||
)
|
||||
Event.new(
|
||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||
message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}",
|
||||
).save()
|
||||
message=f"Task {self.__name__} encountered an error",
|
||||
).with_exception(exc).save()
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -56,7 +56,6 @@ from authentik.flows.planner import (
|
||||
)
|
||||
from authentik.flows.stage import AccessDeniedStage, StageView
|
||||
from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
@@ -239,8 +238,8 @@ class FlowExecutorView(APIView):
|
||||
capture_exception(exc)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=exception_to_string(exc),
|
||||
).from_http(self.request)
|
||||
message="System exception during flow execution.",
|
||||
).with_exception(exc).from_http(self.request)
|
||||
challenge = FlowErrorChallenge(self.request, exc)
|
||||
challenge.is_valid(raise_exception=True)
|
||||
return to_stage_response(self.request, HttpChallengeResponse(challenge))
|
||||
|
||||
@@ -14,7 +14,6 @@ from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.expression.exceptions import ControlFlowException
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import Model
|
||||
@@ -106,9 +105,9 @@ class BaseOutgoingSyncClient[
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
message="Failed to evaluate property-mapping",
|
||||
mapping=exc.mapping,
|
||||
).save()
|
||||
).with_exception(exc).save()
|
||||
raise StopSync(exc, obj, exc.mapping) from exc
|
||||
if not raw_final_object:
|
||||
raise StopSync(ValueError("No mappings configured"), obj)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from traceback import extract_tb
|
||||
|
||||
from structlog.tracebacks import ExceptionDictTransformer
|
||||
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
|
||||
TRACEBACK_HEADER = "Traceback (most recent call last):"
|
||||
@@ -17,3 +19,8 @@ def exception_to_string(exc: Exception) -> str:
|
||||
f"{class_to_path(exc.__class__)}: {str(exc)}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def exception_to_dict(exc: Exception) -> dict:
|
||||
"""Format exception as a dictionary"""
|
||||
return ExceptionDictTransformer()((type(exc), exc, exc.__traceback__))
|
||||
|
||||
@@ -35,7 +35,6 @@ from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
@@ -326,9 +325,8 @@ class Outpost(SerializerModel, ManagedModel):
|
||||
"While setting the permissions for the service-account, a "
|
||||
"permission was not found: Check "
|
||||
"https://goauthentik.io/docs/troubleshooting/missing_permission"
|
||||
)
|
||||
+ exception_to_string(exc),
|
||||
).set_user(user).save()
|
||||
),
|
||||
).with_exception(exc).set_user(user).save()
|
||||
else:
|
||||
app_label, perm = model_or_perm.split(".")
|
||||
permission = Permission.objects.filter(
|
||||
|
||||
@@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.policies.apps import HIST_POLICIES_EXECUTION_TIME
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
@@ -95,10 +95,13 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
except PolicyException as exc:
|
||||
# Either use passed original exception or whatever we have
|
||||
src_exc = exc.src_exc if exc.src_exc else exc
|
||||
error_string = exception_to_string(src_exc)
|
||||
# Create policy exception event, only when we're not debugging
|
||||
if not self.request.debug:
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
self.create_event(
|
||||
EventAction.POLICY_EXCEPTION,
|
||||
message="Policy failed to execute",
|
||||
exception=exception_to_dict(src_exc),
|
||||
)
|
||||
LOGGER.debug("P_ENG(proc): error, using failure result", exc=src_exc)
|
||||
policy_result = PolicyResult(self.binding.failure_result, str(src_exc))
|
||||
policy_result.source_binding = self.binding
|
||||
@@ -143,5 +146,5 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
try:
|
||||
self.connection.send(self.profiling_wrapper())
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Policy failed to run", exc=exception_to_string(exc))
|
||||
LOGGER.warning("Policy failed to run", exc=exc)
|
||||
self.connection.send(PolicyResult(False, str(exc)))
|
||||
|
||||
@@ -241,4 +241,4 @@ class TestPolicyProcess(TestCase):
|
||||
self.assertEqual(len(events), 1)
|
||||
event = events.first()
|
||||
self.assertEqual(event.user["username"], self.user.username)
|
||||
self.assertIn("division by zero", event.context["message"])
|
||||
self.assertIn("Policy failed to execute", event.context["message"])
|
||||
|
||||
@@ -15,12 +15,14 @@ class OAuth2Error(SentryIgnoredException):
|
||||
|
||||
error: str
|
||||
description: str
|
||||
cause: str | None = None
|
||||
|
||||
def create_dict(self):
|
||||
def create_dict(self, request: HttpRequest):
|
||||
"""Return error as dict for JSON Rendering"""
|
||||
return {
|
||||
"error": self.error,
|
||||
"error_description": self.description,
|
||||
"request_id": request.request_id,
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -31,9 +33,15 @@ class OAuth2Error(SentryIgnoredException):
|
||||
return Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=message or self.description,
|
||||
cause=self.cause,
|
||||
error=self.error,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def with_cause(self, cause: str):
|
||||
self.cause = cause
|
||||
return self
|
||||
|
||||
|
||||
class RedirectUriError(OAuth2Error):
|
||||
"""The request fails due to a missing, invalid, or mismatching
|
||||
@@ -243,13 +251,14 @@ class TokenRevocationError(OAuth2Error):
|
||||
self.description = self.errors[error]
|
||||
|
||||
|
||||
class DeviceCodeError(OAuth2Error):
|
||||
class DeviceCodeError(TokenError):
|
||||
"""
|
||||
Device-code flow errors
|
||||
See https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
|
||||
Can also use codes form TokenError
|
||||
"""
|
||||
|
||||
errors = {
|
||||
errors = TokenError.errors | {
|
||||
"authorization_pending": (
|
||||
"The authorization request is still pending as the end user hasn't "
|
||||
"yet completed the user-interaction steps"
|
||||
@@ -261,10 +270,15 @@ class DeviceCodeError(OAuth2Error):
|
||||
"authorization request but SHOULD wait for user interaction before "
|
||||
"restarting to avoid unnecessary polling."
|
||||
),
|
||||
"slow_down": (
|
||||
'A variant of "authorization_pending", the authorization request is'
|
||||
"still pending and polling should continue, but the interval MUST"
|
||||
"be increased by 5 seconds for this and all subsequent requests."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, error: str):
|
||||
super().__init__()
|
||||
super().__init__(error)
|
||||
self.error = error
|
||||
self.description = self.errors[error]
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
||||
from authentik.providers.oauth2.constants import SCOPE_OFFLINE_ACCESS, SCOPE_OPENID, TOKEN_TYPE
|
||||
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
@@ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -53,6 +53,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "unsupported_response_type")
|
||||
|
||||
def test_invalid_client_id(self):
|
||||
"""Test invalid client ID"""
|
||||
@@ -68,7 +69,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -79,19 +80,30 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "request_not_supported")
|
||||
|
||||
def test_invalid_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
def test_invalid_redirect_uri_missing(self):
|
||||
"""test missing redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_missing")
|
||||
|
||||
def test_invalid_redirect_uri(self):
|
||||
"""test invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -101,6 +113,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_blocked_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
@@ -108,9 +121,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:localhost")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -120,6 +133,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_forbidden_scheme")
|
||||
|
||||
def test_invalid_redirect_uri_empty(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
@@ -129,9 +143,6 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -150,12 +161,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "http://local.invalid?")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -165,6 +173,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_redirect_uri_invalid_regex(self):
|
||||
"""test missing/invalid redirect URI (invalid regex)"""
|
||||
@@ -172,12 +181,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "+")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -187,23 +193,22 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_empty_redirect_uri(self):
|
||||
"""test empty redirect URI (configure in provider)"""
|
||||
def test_redirect_uri_regex(self):
|
||||
"""test valid redirect URI (regex)"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, ".+")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"redirect_uri": "http://foo.bar.baz",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
@@ -258,7 +263,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
GrantTypes.IMPLICIT,
|
||||
)
|
||||
# Implicit without openid scope
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -285,7 +290,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@@ -295,6 +300,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "unsupported_response_type")
|
||||
|
||||
def test_full_code(self):
|
||||
"""Test full authorization"""
|
||||
@@ -613,3 +619,54 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_openid_missing_invalid(self):
|
||||
"""test request requiring an OpenID scope to be set"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"scope": "",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "scope_openid_missing")
|
||||
|
||||
@apply_blueprint("system/providers-oauth2.yaml")
|
||||
def test_offline_access_invalid(self):
|
||||
"""test request for offline_access with invalid response type"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
managed__in=[
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
"goauthentik.io/providers/oauth2/scope-offline_access",
|
||||
]
|
||||
)
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"scope": f"{SCOPE_OPENID} {SCOPE_OFFLINE_ACCESS}",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
parsed = OAuthAuthorizationParams.from_request(request)
|
||||
self.assertNotIn(SCOPE_OFFLINE_ACCESS, parsed.scope)
|
||||
|
||||
@@ -68,7 +68,11 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_no_provider(self):
|
||||
@@ -87,7 +91,11 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_permission_denied(self):
|
||||
@@ -110,7 +118,11 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_incorrect_scopes(self):
|
||||
|
||||
@@ -68,7 +68,11 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_wrong_token(self):
|
||||
@@ -85,7 +89,11 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_no_provider(self):
|
||||
@@ -104,7 +112,11 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_permission_denied(self):
|
||||
@@ -127,7 +139,11 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_successful(self):
|
||||
|
||||
@@ -68,7 +68,11 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_wrong_token(self):
|
||||
@@ -86,7 +90,11 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_no_provider(self):
|
||||
@@ -106,7 +114,11 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_permission_denied(self):
|
||||
@@ -130,7 +142,11 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": TokenError.errors["invalid_grant"],
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_successful(self):
|
||||
|
||||
@@ -80,6 +80,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
"revoked, does not match the redirection URI used in the authorization "
|
||||
"request, or was issued to another client"
|
||||
),
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
@@ -136,6 +137,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
"revoked, does not match the redirection URI used in the authorization "
|
||||
"request, or was issued to another client"
|
||||
),
|
||||
"request_id": response.headers["X-authentik-id"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@@ -190,7 +190,7 @@ class OAuthAuthorizationParams:
|
||||
allowed_redirect_urls = self.provider.redirect_uris
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError("", allowed_redirect_urls)
|
||||
raise RedirectUriError("", allowed_redirect_urls).with_cause("redirect_uri_missing")
|
||||
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
||||
@@ -219,10 +219,14 @@ class OAuthAuthorizationParams:
|
||||
provider=self.provider,
|
||||
)
|
||||
if not match_found:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause(
|
||||
"redirect_uri_no_match"
|
||||
)
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause(
|
||||
"redirect_uri_forbidden_scheme"
|
||||
)
|
||||
|
||||
def check_scope(self, github_compat=False):
|
||||
"""Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
|
||||
@@ -251,7 +255,9 @@ class OAuthAuthorizationParams:
|
||||
or self.response_type in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN]
|
||||
):
|
||||
LOGGER.warning("Missing 'openid' scope.")
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "invalid_scope", self.grant_type, self.state
|
||||
).with_cause("scope_openid_missing")
|
||||
if SCOPE_OFFLINE_ACCESS in self.scope:
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
# Don't explicitly request consent with offline_access, as the spec allows for
|
||||
@@ -286,7 +292,9 @@ class OAuthAuthorizationParams:
|
||||
return
|
||||
if not self.nonce:
|
||||
LOGGER.warning("Missing nonce for OpenID Request")
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "invalid_request", self.grant_type, self.state
|
||||
).with_cause("nonce_missing")
|
||||
|
||||
def check_code_challenge(self):
|
||||
"""PKCE validation of the transformation method."""
|
||||
@@ -345,10 +353,10 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||
self.request, github_compat=self.github_compat
|
||||
)
|
||||
except AuthorizeError as error:
|
||||
LOGGER.warning(error.description, redirect_uri=error.redirect_uri)
|
||||
LOGGER.warning(error.description, redirect_uri=error.redirect_uri, cause=error.cause)
|
||||
raise RequestValidationError(error.get_response(self.request)) from None
|
||||
except OAuth2Error as error:
|
||||
LOGGER.warning(error.description)
|
||||
LOGGER.warning(error.description, cause=error.cause)
|
||||
raise RequestValidationError(
|
||||
bad_request_message(self.request, error.description, title=error.error)
|
||||
) from None
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
@@ -14,7 +14,9 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.models import Application
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.errors import DeviceCodeError
|
||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||
from authentik.providers.oauth2.utils import TokenResponse
|
||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
|
||||
|
||||
LOGGER = get_logger()
|
||||
@@ -28,38 +30,36 @@ class DeviceView(View):
|
||||
provider: OAuth2Provider
|
||||
scopes: list[str] = []
|
||||
|
||||
def parse_request(self) -> HttpResponse | None:
|
||||
def parse_request(self):
|
||||
"""Parse incoming request"""
|
||||
client_id = self.request.POST.get("client_id", None)
|
||||
if not client_id:
|
||||
return HttpResponseBadRequest()
|
||||
provider = OAuth2Provider.objects.filter(
|
||||
client_id=client_id,
|
||||
).first()
|
||||
raise DeviceCodeError("invalid_client")
|
||||
provider = OAuth2Provider.objects.filter(client_id=client_id).first()
|
||||
if not provider:
|
||||
return HttpResponseBadRequest()
|
||||
raise DeviceCodeError("invalid_client")
|
||||
try:
|
||||
_ = provider.application
|
||||
except Application.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
raise DeviceCodeError("invalid_client") from None
|
||||
self.provider = provider
|
||||
self.client_id = client_id
|
||||
self.scopes = self.request.POST.get("scope", "").split(" ")
|
||||
return None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
throttle = AnonRateThrottle()
|
||||
throttle.rate = CONFIG.get("throttle.providers.oauth2.device", "20/hour")
|
||||
throttle.num_requests, throttle.duration = throttle.parse_rate(throttle.rate)
|
||||
if not throttle.allow_request(request, self):
|
||||
return HttpResponse(status=429)
|
||||
return TokenResponse(DeviceCodeError("slow_down").create_dict(request), status=429)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Generate device token"""
|
||||
resp = self.parse_request()
|
||||
if resp:
|
||||
return resp
|
||||
try:
|
||||
self.parse_request()
|
||||
except DeviceCodeError as exc:
|
||||
return TokenResponse(exc.create_dict(request), status=400)
|
||||
until = timedelta_from_string(self.provider.access_code_validity)
|
||||
token: DeviceToken = DeviceToken.objects.create(
|
||||
expires=now() + until, provider=self.provider, _scope=" ".join(self.scopes)
|
||||
@@ -67,7 +67,7 @@ class DeviceView(View):
|
||||
device_url = self.request.build_absolute_uri(
|
||||
reverse("authentik_providers_oauth2_root:device-login")
|
||||
)
|
||||
return JsonResponse(
|
||||
return TokenResponse(
|
||||
{
|
||||
"device_code": token.device_code,
|
||||
"verification_uri": device_url,
|
||||
|
||||
@@ -598,9 +598,9 @@ class TokenView(View):
|
||||
return TokenResponse(self.create_device_code_response())
|
||||
raise TokenError("unsupported_grant_type")
|
||||
except (TokenError, DeviceCodeError) as error:
|
||||
return TokenResponse(error.create_dict(), status=400)
|
||||
return TokenResponse(error.create_dict(request), status=400)
|
||||
except UserAuthError as error:
|
||||
return TokenResponse(error.create_dict(), status=403)
|
||||
return TokenResponse(error.create_dict(request), status=403)
|
||||
|
||||
def create_code_response(self) -> dict[str, Any]:
|
||||
"""See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1"""
|
||||
|
||||
@@ -65,7 +65,7 @@ class TokenRevokeView(View):
|
||||
|
||||
return TokenResponse(data={}, status=200)
|
||||
except TokenRevocationError as exc:
|
||||
return TokenResponse(exc.create_dict(), status=401)
|
||||
return TokenResponse(exc.create_dict(request), status=401)
|
||||
except Http404:
|
||||
# Token not found should return a HTTP 200
|
||||
# https://datatracker.ietf.org/doc/html/rfc7009#section-2.2
|
||||
|
||||
@@ -102,6 +102,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
# Buffer sizes for large headers with JWTs
|
||||
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
|
||||
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
|
||||
"nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
|
||||
# Enable TLS in traefik
|
||||
"traefik.ingress.kubernetes.io/router.tls": "true",
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.expression.exceptions import ControlFlowException
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyResult
|
||||
@@ -142,9 +141,9 @@ class RadiusOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
message="Failed to evaluate property-mapping",
|
||||
mapping=exc.mapping,
|
||||
).save()
|
||||
).with_exception(exc).save()
|
||||
return None
|
||||
return b64encode(packet.RequestPacket()).decode()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
|
||||
from pydanticscim.group import Group as BaseGroup
|
||||
from pydanticscim.responses import PatchOperation as BasePatchOperation
|
||||
from pydanticscim.responses import PatchRequest as BasePatchRequest
|
||||
@@ -12,19 +12,95 @@ from pydanticscim.service_provider import ChangePassword, Filter, Patch, Sort
|
||||
from pydanticscim.service_provider import (
|
||||
ServiceProviderConfiguration as BaseServiceProviderConfiguration,
|
||||
)
|
||||
from pydanticscim.user import AddressKind
|
||||
from pydanticscim.user import User as BaseUser
|
||||
|
||||
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
|
||||
|
||||
class Address(BaseModel):
|
||||
formatted: str | None = Field(
|
||||
None,
|
||||
description="The full mailing address, formatted for display "
|
||||
"or use with a mailing label. This attribute MAY contain newlines.",
|
||||
)
|
||||
streetAddress: str | None = Field(
|
||||
None,
|
||||
description="The full street address component, which may "
|
||||
"include house number, street name, P.O. box, and multi-line "
|
||||
"extended street address information. This attribute MAY contain newlines.",
|
||||
)
|
||||
locality: str | None = Field(None, description="The city or locality component.")
|
||||
region: str | None = Field(None, description="The state or region component.")
|
||||
postalCode: str | None = Field(None, description="The zip code or postal code component.")
|
||||
country: str | None = Field(None, description="The country name component.")
|
||||
type: AddressKind | None = Field(
|
||||
None,
|
||||
description="A label indicating the attribute's function, e.g., 'work' or 'home'.",
|
||||
)
|
||||
primary: bool | None = None
|
||||
|
||||
|
||||
class Manager(BaseModel):
|
||||
value: str | None = Field(
|
||||
None,
|
||||
description="The id of the SCIM resource representingthe User's manager. REQUIRED.",
|
||||
)
|
||||
ref: AnyUrl | None = Field(
|
||||
None,
|
||||
alias="$ref",
|
||||
description="The URI of the SCIM resource representing the User's manager. REQUIRED.",
|
||||
)
|
||||
displayName: str | None = Field(
|
||||
None,
|
||||
description="The displayName of the User's manager. OPTIONAL and READ-ONLY.",
|
||||
)
|
||||
|
||||
|
||||
class EnterpriseUser(BaseModel):
|
||||
employeeNumber: str | None = Field(
|
||||
None,
|
||||
description="Numeric or alphanumeric identifier assigned to a person, "
|
||||
"typically based on order of hire or association with anorganization.",
|
||||
)
|
||||
costCenter: str | None = Field(None, description="Identifies the name of a cost center.")
|
||||
organization: str | None = Field(None, description="Identifies the name of an organization.")
|
||||
division: str | None = Field(None, description="Identifies the name of a division.")
|
||||
department: str | None = Field(
|
||||
None,
|
||||
description="Numeric or alphanumeric identifier assigned to a person,"
|
||||
" typically based on order of hire or association with anorganization.",
|
||||
)
|
||||
manager: Manager | None = Field(
|
||||
None,
|
||||
description="The User's manager. A complex type that optionally allows "
|
||||
"service providers to represent organizational hierarchy by referencing"
|
||||
" the 'id' attribute of another User.",
|
||||
)
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
model_config = ConfigDict(serialize_by_alias=True)
|
||||
|
||||
id: str | int | None = None
|
||||
schemas: list[str] = [SCIM_USER_SCHEMA]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
addresses: list[Address] | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"A physical mailing address for this User. Canonical type "
|
||||
"values of 'work', 'home', and 'other'."
|
||||
),
|
||||
)
|
||||
enterprise_user: EnterpriseUser | None = Field(
|
||||
default=None,
|
||||
alias="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
|
||||
serialization_alias="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
|
||||
)
|
||||
|
||||
|
||||
class Group(BaseGroup):
|
||||
@@ -92,7 +168,7 @@ class PatchOperation(BasePatchOperation):
|
||||
"""PatchOperation with optional path"""
|
||||
|
||||
op: PatchOp
|
||||
path: str | None
|
||||
path: str | None = None
|
||||
|
||||
|
||||
class SCIMError(BaseSCIMError):
|
||||
|
||||
@@ -28,7 +28,6 @@ from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.sentry import should_ignore_exception
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
@@ -83,8 +82,8 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
|
||||
CTX_TASK_ID.set(...)
|
||||
if not should_ignore_exception(exception):
|
||||
Event.new(
|
||||
EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id
|
||||
).save()
|
||||
EventAction.SYSTEM_EXCEPTION, message="Failed to execute task", task_id=task_id
|
||||
).with_exception(exception).save()
|
||||
|
||||
|
||||
def _get_startup_tasks_default_tenant() -> list[Callable]:
|
||||
|
||||
@@ -8,7 +8,6 @@ from authentik.events.models import TaskStatus
|
||||
from authentik.events.system_tasks import SystemTask
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.sync import KerberosSync
|
||||
@@ -64,5 +63,5 @@ def kerberos_sync_single(self, source_pk: str):
|
||||
syncer.sync()
|
||||
self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages)
|
||||
except StopSync as exc:
|
||||
LOGGER.warning(exception_to_string(exc))
|
||||
LOGGER.warning("Error syncing kerberos", exc=exc, source=source)
|
||||
self.set_error(exc)
|
||||
|
||||
@@ -12,7 +12,6 @@ from authentik.events.models import TaskStatus
|
||||
from authentik.events.system_tasks import SystemTask
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
@@ -149,5 +148,5 @@ def ldap_sync(self: SystemTask, source_pk: str, sync_class: str, page_cache_key:
|
||||
cache.delete(page_cache_key)
|
||||
except (LDAPException, StopSync) as exc:
|
||||
# No explicit event is created here as .set_status with an error will do that
|
||||
LOGGER.warning(exception_to_string(exc))
|
||||
LOGGER.warning("Failed to sync LDAP", exc=exc, source=source)
|
||||
self.set_error(exc)
|
||||
|
||||
@@ -10,6 +10,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.apple",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.entra_id",
|
||||
"authentik.sources.oauth.types.facebook",
|
||||
"authentik.sources.oauth.types.github",
|
||||
"authentik.sources.oauth.types.gitlab",
|
||||
|
||||
@@ -232,7 +232,7 @@ class GoogleOAuthSource(CreatableType, OAuthSource):
|
||||
|
||||
|
||||
class AzureADOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Azure AD."""
|
||||
"""(Deprecated) Social Login using Azure AD."""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -240,6 +240,17 @@ class AzureADOAuthSource(CreatableType, OAuthSource):
|
||||
verbose_name_plural = _("Azure AD OAuth Sources")
|
||||
|
||||
|
||||
# TODO: When removing this, add a migration for OAuthSource that sets
|
||||
# provider_type to `entraid` if it is currently `azuread`
|
||||
class EntraIDOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Entra ID."""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("Entra ID OAuth Source")
|
||||
verbose_name_plural = _("Entra ID OAuth Sources")
|
||||
|
||||
|
||||
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
|
||||
"""Login using a Generic OpenID-Connect compliant provider."""
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""azure ad Type tests"""
|
||||
"""Entra ID Type tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.azure_ad import AzureADOAuthCallback, AzureADType
|
||||
from authentik.sources.oauth.types.entra_id import EntraIDOAuthCallback, EntraIDType
|
||||
|
||||
# https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#response-2
|
||||
AAD_USER = {
|
||||
EID_USER = {
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
|
||||
"@odata.id": (
|
||||
"https://graph.microsoft.com/v2/7ce9b89e-646a-41d2-9fa6-8371c6a8423d/"
|
||||
@@ -41,11 +41,11 @@ class TestTypeAzureAD(TestCase):
|
||||
|
||||
def test_enroll_context(self):
|
||||
"""Test azure_ad Enrollment context"""
|
||||
ak_context = AzureADType().get_base_user_properties(source=self.source, info=AAD_USER)
|
||||
self.assertEqual(ak_context["username"], AAD_USER["userPrincipalName"])
|
||||
self.assertEqual(ak_context["email"], AAD_USER["mail"])
|
||||
self.assertEqual(ak_context["name"], AAD_USER["displayName"])
|
||||
ak_context = EntraIDType().get_base_user_properties(source=self.source, info=EID_USER)
|
||||
self.assertEqual(ak_context["username"], EID_USER["userPrincipalName"])
|
||||
self.assertEqual(ak_context["email"], EID_USER["mail"])
|
||||
self.assertEqual(ak_context["name"], EID_USER["displayName"])
|
||||
|
||||
def test_user_id(self):
|
||||
"""Test azure AD user ID"""
|
||||
self.assertEqual(AzureADOAuthCallback().get_user_id(AAD_USER), AAD_USER["id"])
|
||||
"""Test Entra ID user ID"""
|
||||
self.assertEqual(EntraIDOAuthCallback().get_user_id(EID_USER), EID_USER["id"])
|
||||
@@ -1,105 +1,17 @@
|
||||
"""AzureAD OAuth2 Views"""
|
||||
|
||||
from typing import Any
|
||||
from authentik.sources.oauth.types.entra_id import EntraIDType
|
||||
from authentik.sources.oauth.types.registry import registry
|
||||
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AzureADOAuthRedirect(OAuthRedirect):
|
||||
"""Azure AD OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": ["openid", "https://graph.microsoft.com/User.Read"],
|
||||
}
|
||||
|
||||
|
||||
class AzureADClient(UserprofileHeaderAuthClient):
|
||||
"""Fetch AzureAD group information"""
|
||||
|
||||
def get_profile_info(self, token):
|
||||
profile_data = super().get_profile_info(token)
|
||||
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
|
||||
return profile_data
|
||||
group_response = self.session.request(
|
||||
"get",
|
||||
"https://graph.microsoft.com/v1.0/me/memberOf",
|
||||
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
|
||||
)
|
||||
try:
|
||||
group_response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch user profile",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response else str(exc),
|
||||
)
|
||||
return None
|
||||
profile_data["raw_groups"] = group_response.json()
|
||||
return profile_data
|
||||
|
||||
|
||||
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
client_class = AzureADClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
# Default try to get `id` for the Graph API endpoint
|
||||
# fallback to OpenID logic in case the profile URL was changed
|
||||
return info.get("id", super().get_user_id(info))
|
||||
# TODO: When removing this, add a migration for OAuthSource that sets
|
||||
# provider_type to `entraid` if it is currently `azuread`
|
||||
|
||||
|
||||
@registry.register()
|
||||
class AzureADType(SourceType):
|
||||
class AzureADType(EntraIDType):
|
||||
"""Azure AD Type definition"""
|
||||
|
||||
callback_view = AzureADOAuthCallback
|
||||
redirect_view = AzureADOAuthRedirect
|
||||
verbose_name = "Azure AD"
|
||||
name = "azuread"
|
||||
|
||||
urls_customizable = True
|
||||
|
||||
authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec
|
||||
profile_url = "https://graph.microsoft.com/v1.0/me"
|
||||
oidc_well_known_url = (
|
||||
"https://login.microsoftonline.com/common/.well-known/openid-configuration"
|
||||
)
|
||||
oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys"
|
||||
|
||||
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
|
||||
|
||||
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||
# Format group info
|
||||
groups = []
|
||||
group_id_dict = {}
|
||||
for group in info.get("raw_groups", {}).get("value", []):
|
||||
if group["@odata.type"] != "#microsoft.graph.group":
|
||||
continue
|
||||
groups.append(group["id"])
|
||||
group_id_dict[group["id"]] = group
|
||||
info["raw_groups"] = group_id_dict
|
||||
return {
|
||||
"username": info.get("userPrincipalName"),
|
||||
"email": mail,
|
||||
"name": info.get("displayName"),
|
||||
"groups": groups,
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, source, group_id, **kwargs):
|
||||
raw_group = kwargs["info"]["raw_groups"][group_id]
|
||||
return {
|
||||
"name": raw_group["displayName"],
|
||||
}
|
||||
|
||||
102
authentik/sources/oauth/types/entra_id.py
Normal file
102
authentik/sources/oauth/types/entra_id.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""EntraID OAuth2 Views"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class EntraIDOAuthRedirect(OAuthRedirect):
|
||||
"""Entra ID OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": ["openid", "https://graph.microsoft.com/User.Read"],
|
||||
}
|
||||
|
||||
|
||||
class EntraIDClient(UserprofileHeaderAuthClient):
|
||||
"""Fetch EntraID group information"""
|
||||
|
||||
def get_profile_info(self, token):
|
||||
profile_data = super().get_profile_info(token)
|
||||
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
|
||||
return profile_data
|
||||
group_response = self.session.request(
|
||||
"get",
|
||||
"https://graph.microsoft.com/v1.0/me/memberOf",
|
||||
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
|
||||
)
|
||||
try:
|
||||
group_response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch user profile",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response else str(exc),
|
||||
)
|
||||
return None
|
||||
profile_data["raw_groups"] = group_response.json()
|
||||
return profile_data
|
||||
|
||||
|
||||
class EntraIDOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||
"""EntraID OAuth2 Callback"""
|
||||
|
||||
client_class = EntraIDClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
# Default try to get `id` for the Graph API endpoint
|
||||
# fallback to OpenID logic in case the profile URL was changed
|
||||
return info.get("id", super().get_user_id(info))
|
||||
|
||||
|
||||
@registry.register()
|
||||
class EntraIDType(SourceType):
|
||||
"""Entra ID Type definition"""
|
||||
|
||||
callback_view = EntraIDOAuthCallback
|
||||
redirect_view = EntraIDOAuthRedirect
|
||||
verbose_name = "Entra ID"
|
||||
name = "entraid"
|
||||
|
||||
urls_customizable = True
|
||||
|
||||
authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec
|
||||
profile_url = "https://graph.microsoft.com/v1.0/me"
|
||||
oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys"
|
||||
|
||||
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
|
||||
|
||||
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||
# Format group info
|
||||
groups = []
|
||||
group_id_dict = {}
|
||||
for group in info.get("raw_groups", {}).get("value", []):
|
||||
if group["@odata.type"] != "#microsoft.graph.group":
|
||||
continue
|
||||
groups.append(group["id"])
|
||||
group_id_dict[group["id"]] = group
|
||||
info["raw_groups"] = group_id_dict
|
||||
return {
|
||||
"username": info.get("userPrincipalName"),
|
||||
"email": mail,
|
||||
"name": info.get("displayName"),
|
||||
"groups": groups,
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, source, group_id, **kwargs):
|
||||
raw_group = kwargs["info"]["raw_groups"][group_id]
|
||||
return {
|
||||
"name": raw_group["displayName"],
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class SCIMSourceGroupSerializer(SourceSerializer):
|
||||
model = SCIMSourceGroup
|
||||
fields = [
|
||||
"id",
|
||||
"external_id",
|
||||
"group",
|
||||
"group_obj",
|
||||
"source",
|
||||
@@ -31,5 +32,5 @@ class SCIMSourceGroupViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = SCIMSourceGroup.objects.all().select_related("group")
|
||||
serializer_class = SCIMSourceGroupSerializer
|
||||
filterset_fields = ["source__slug", "group__name", "group__group_uuid"]
|
||||
search_fields = ["source__slug", "group__name", "attributes"]
|
||||
search_fields = ["source__slug", "group__name", "attributes", "external_id"]
|
||||
ordering = ["group__name"]
|
||||
|
||||
@@ -18,6 +18,7 @@ class SCIMSourceUserSerializer(SourceSerializer):
|
||||
model = SCIMSourceUser
|
||||
fields = [
|
||||
"id",
|
||||
"external_id",
|
||||
"user",
|
||||
"user_obj",
|
||||
"source",
|
||||
@@ -31,5 +32,5 @@ class SCIMSourceUserViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = SCIMSourceUser.objects.all().select_related("user")
|
||||
serializer_class = SCIMSourceUserSerializer
|
||||
filterset_fields = ["source__slug", "user__username", "user__id"]
|
||||
search_fields = ["source__slug", "user__username", "attributes"]
|
||||
search_fields = ["source__slug", "user__username", "attributes", "user__uuid", "external_id"]
|
||||
ordering = ["user__username"]
|
||||
|
||||
4
authentik/sources/scim/constants.py
Normal file
4
authentik/sources/scim/constants.py
Normal file
@@ -0,0 +1,4 @@
|
||||
SCIM_URN_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Schema"
|
||||
SCIM_URN_GROUP = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
SCIM_URN_USER = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
SCIM_URN_USER_ENTERPRISE = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
||||
@@ -1,8 +0,0 @@
|
||||
"""SCIM Errors"""
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
|
||||
class PatchError(SentryIgnoredException):
|
||||
"""Error raised within an atomic block when an error happened
|
||||
so nothing is saved"""
|
||||
@@ -0,0 +1,98 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-13 01:07
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
from django.apps.registry import Apps
|
||||
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_ext_id(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
SCIMSourceUser = apps.get_model("authentik_sources_scim", "SCIMSourceUser")
|
||||
SCIMSourceGroup = apps.get_model("authentik_sources_scim", "SCIMSourceGroup")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for user in SCIMSourceUser.objects.using(db_alias).all():
|
||||
user.external_id = user.id
|
||||
user.save(update_fields=["external_id"])
|
||||
for group in SCIMSourceGroup.objects.using(db_alias).all():
|
||||
group.external_id = group.id
|
||||
group.save(update_fields=["external_id"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_scim", "0002_scimsourcepropertymapping"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimsourcegroup",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimsourceuser",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimsourcegroup",
|
||||
name="external_id",
|
||||
field=models.TextField(default=None, null=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimsourceuser",
|
||||
name="external_id",
|
||||
field=models.TextField(default=None, null=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimsourcegroup",
|
||||
unique_together={("external_id", "source")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimsourceuser",
|
||||
unique_together={("external_id", "source")},
|
||||
),
|
||||
migrations.RunPython(migrate_ext_id, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name="scimsourcegroup",
|
||||
name="external_id",
|
||||
field=models.TextField(),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimsourceuser",
|
||||
name="external_id",
|
||||
field=models.TextField(),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="scimsourcegroup",
|
||||
index=models.Index(fields=["external_id"], name="authentik_s_externa_05e346_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="scimsourceuser",
|
||||
index=models.Index(fields=["external_id"], name="authentik_s_externa_4bd760_idx"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimsourcegroup",
|
||||
name="id",
|
||||
field=models.TextField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimsourceuser",
|
||||
name="id",
|
||||
field=models.TextField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimsourcegroup",
|
||||
name="last_update",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimsourceuser",
|
||||
name="last_update",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
"""SCIM Source"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
@@ -103,10 +104,12 @@ class SCIMSourcePropertyMapping(PropertyMapping):
|
||||
class SCIMSourceUser(SerializerModel):
|
||||
"""Mapping of a user and source to a SCIM user ID"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
id = models.TextField(primary_key=True, default=uuid4)
|
||||
external_id = models.TextField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(SCIMSource, on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
last_update = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
@@ -115,7 +118,10 @@ class SCIMSourceUser(SerializerModel):
|
||||
return SCIMSourceUserSerializer
|
||||
|
||||
class Meta:
|
||||
unique_together = (("id", "user", "source"),)
|
||||
unique_together = (("external_id", "source"),)
|
||||
indexes = [
|
||||
models.Index(fields=["external_id"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM User {self.user_id} to {self.source_id}"
|
||||
@@ -124,10 +130,12 @@ class SCIMSourceUser(SerializerModel):
|
||||
class SCIMSourceGroup(SerializerModel):
|
||||
"""Mapping of a group and source to a SCIM user ID"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
id = models.TextField(primary_key=True, default=uuid4)
|
||||
external_id = models.TextField()
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(SCIMSource, on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
last_update = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
@@ -136,7 +144,10 @@ class SCIMSourceGroup(SerializerModel):
|
||||
return SCIMSourceGroupSerializer
|
||||
|
||||
class Meta:
|
||||
unique_together = (("id", "group", "source"),)
|
||||
unique_together = (("external_id", "source"),)
|
||||
indexes = [
|
||||
models.Index(fields=["external_id"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM Group {self.group_id} to {self.source_id}"
|
||||
|
||||
0
authentik/sources/scim/patch/__init__.py
Normal file
0
authentik/sources/scim/patch/__init__.py
Normal file
180
authentik/sources/scim/patch/lexer.py
Normal file
180
authentik/sources/scim/patch/lexer.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from authentik.sources.scim.constants import (
|
||||
SCIM_URN_GROUP,
|
||||
SCIM_URN_SCHEMA,
|
||||
SCIM_URN_USER,
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
)
|
||||
|
||||
|
||||
# Token types for SCIM path parsing
|
||||
class TokenType(Enum):
|
||||
ATTRIBUTE = "ATTRIBUTE"
|
||||
DOT = "DOT"
|
||||
LBRACKET = "LBRACKET"
|
||||
RBRACKET = "RBRACKET"
|
||||
LPAREN = "LPAREN"
|
||||
RPAREN = "RPAREN"
|
||||
STRING = "STRING"
|
||||
NUMBER = "NUMBER"
|
||||
BOOLEAN = "BOOLEAN"
|
||||
NULL = "NULL"
|
||||
OPERATOR = "OPERATOR"
|
||||
AND = "AND"
|
||||
OR = "OR"
|
||||
NOT = "NOT"
|
||||
EOF = "EOF"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
type: TokenType
|
||||
value: str
|
||||
position: int = 0
|
||||
|
||||
|
||||
class SCIMPathLexer:
|
||||
"""Lexer for SCIM paths and filter expressions"""
|
||||
|
||||
OPERATORS = ["eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le", "pr"]
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.schema_urns = [
|
||||
SCIM_URN_SCHEMA,
|
||||
SCIM_URN_GROUP,
|
||||
SCIM_URN_USER,
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
]
|
||||
self.text = text
|
||||
self.pos = 0
|
||||
self.current_char = self.text[self.pos] if self.pos < len(self.text) else None
|
||||
|
||||
def advance(self):
|
||||
"""Move to next character"""
|
||||
self.pos += 1
|
||||
self.current_char = self.text[self.pos] if self.pos < len(self.text) else None
|
||||
|
||||
def skip_whitespace(self):
|
||||
"""Skip whitespace characters"""
|
||||
while self.current_char and self.current_char.isspace():
|
||||
self.advance()
|
||||
|
||||
def read_string(self, quote_char):
|
||||
"""Read a quoted string"""
|
||||
value = ""
|
||||
self.advance() # Skip opening quote
|
||||
|
||||
while self.current_char and self.current_char != quote_char:
|
||||
if self.current_char == "\\":
|
||||
self.advance()
|
||||
if self.current_char:
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
else:
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
|
||||
if self.current_char == quote_char:
|
||||
self.advance() # Skip closing quote
|
||||
|
||||
return value
|
||||
|
||||
def read_number(self):
|
||||
"""Read a number (integer or float)"""
|
||||
value = ""
|
||||
while self.current_char and (self.current_char.isdigit() or self.current_char == "."):
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
return value
|
||||
|
||||
def read_identifier(self):
|
||||
"""Read an identifier (attribute name or operator) - supports URN format"""
|
||||
value = ""
|
||||
while self.current_char and (self.current_char.isalnum() or self.current_char in "_-:"):
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
# If the identifier value so far is a schema URN, take that as the identifier and
|
||||
# treat the next part as a sub_attribute
|
||||
if value in self.schema_urns:
|
||||
self.current_char = "."
|
||||
return value
|
||||
|
||||
# Handle dots within URN identifiers (like "2.0")
|
||||
# A dot is part of the identifier if it's followed by a digit
|
||||
if (
|
||||
self.current_char == "."
|
||||
and self.pos + 1 < len(self.text)
|
||||
and self.text[self.pos + 1].isdigit()
|
||||
):
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
# Continue reading digits after the dot
|
||||
while self.current_char and self.current_char.isdigit():
|
||||
value += self.current_char
|
||||
self.advance()
|
||||
|
||||
return value
|
||||
|
||||
def get_next_token(self) -> Token: # noqa PLR0911
|
||||
"""Get the next token from the input"""
|
||||
while self.current_char:
|
||||
if self.current_char.isspace():
|
||||
self.skip_whitespace()
|
||||
continue
|
||||
|
||||
if self.current_char == ".":
|
||||
self.advance()
|
||||
return Token(TokenType.DOT, ".")
|
||||
|
||||
if self.current_char == "[":
|
||||
self.advance()
|
||||
return Token(TokenType.LBRACKET, "[")
|
||||
|
||||
if self.current_char == "]":
|
||||
self.advance()
|
||||
return Token(TokenType.RBRACKET, "]")
|
||||
|
||||
if self.current_char == "(":
|
||||
self.advance()
|
||||
return Token(TokenType.LPAREN, "(")
|
||||
|
||||
if self.current_char == ")":
|
||||
self.advance()
|
||||
return Token(TokenType.RPAREN, ")")
|
||||
|
||||
if self.current_char in "\"'":
|
||||
quote_char = self.current_char
|
||||
value = self.read_string(quote_char)
|
||||
return Token(TokenType.STRING, value)
|
||||
|
||||
if self.current_char.isdigit():
|
||||
value = self.read_number()
|
||||
return Token(TokenType.NUMBER, value)
|
||||
|
||||
if self.current_char.isalpha() or self.current_char == "_":
|
||||
value = self.read_identifier()
|
||||
|
||||
# Check for special keywords
|
||||
if value.lower() == "true":
|
||||
return Token(TokenType.BOOLEAN, True)
|
||||
elif value.lower() == "false":
|
||||
return Token(TokenType.BOOLEAN, False)
|
||||
elif value.lower() == "null":
|
||||
return Token(TokenType.NULL, None)
|
||||
elif value.lower() == "and":
|
||||
return Token(TokenType.AND, "and")
|
||||
elif value.lower() == "or":
|
||||
return Token(TokenType.OR, "or")
|
||||
elif value.lower() == "not":
|
||||
return Token(TokenType.NOT, "not")
|
||||
elif value.lower() in self.OPERATORS:
|
||||
return Token(TokenType.OPERATOR, value.lower())
|
||||
else:
|
||||
return Token(TokenType.ATTRIBUTE, value)
|
||||
|
||||
# Skip unknown characters
|
||||
self.advance()
|
||||
|
||||
return Token(TokenType.EOF, "")
|
||||
131
authentik/sources/scim/patch/parser.py
Normal file
131
authentik/sources/scim/patch/parser.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from typing import Any
|
||||
|
||||
from authentik.sources.scim.patch.lexer import SCIMPathLexer, TokenType
|
||||
|
||||
|
||||
class SCIMPathParser:
|
||||
"""Parser for SCIM paths including filter expressions"""
|
||||
|
||||
def __init__(self):
|
||||
self.lexer = None
|
||||
self.current_token = None
|
||||
|
||||
def parse_path(self, path: str | None) -> list[dict[str, Any]]:
|
||||
"""Parse a SCIM path into components"""
|
||||
self.lexer = SCIMPathLexer(path)
|
||||
self.current_token = self.lexer.get_next_token()
|
||||
|
||||
components = []
|
||||
|
||||
while self.current_token.type != TokenType.EOF:
|
||||
component = self._parse_path_component()
|
||||
if component:
|
||||
components.append(component)
|
||||
|
||||
return components
|
||||
|
||||
def _parse_path_component(self) -> dict[str, Any] | None:
|
||||
"""Parse a single path component"""
|
||||
if self.current_token.type != TokenType.ATTRIBUTE:
|
||||
return None
|
||||
|
||||
attribute = self.current_token.value
|
||||
self._consume(TokenType.ATTRIBUTE)
|
||||
|
||||
filter_expr = None
|
||||
sub_attribute = None
|
||||
|
||||
# Check for filter expression
|
||||
if self.current_token.type == TokenType.LBRACKET:
|
||||
self._consume(TokenType.LBRACKET)
|
||||
filter_expr = self._parse_filter_expression()
|
||||
self._consume(TokenType.RBRACKET)
|
||||
|
||||
# Check for sub-attribute
|
||||
if self.current_token.type == TokenType.DOT:
|
||||
self._consume(TokenType.DOT)
|
||||
if self.current_token.type == TokenType.ATTRIBUTE:
|
||||
sub_attribute = self.current_token.value
|
||||
self._consume(TokenType.ATTRIBUTE)
|
||||
|
||||
return {"attribute": attribute, "filter": filter_expr, "sub_attribute": sub_attribute}
|
||||
|
||||
def _parse_filter_expression(self) -> dict[str, Any] | None:
|
||||
"""Parse a filter expression like 'primary eq true' or
|
||||
'type eq "work" and primary eq true'"""
|
||||
return self._parse_or_expression()
|
||||
|
||||
def _parse_or_expression(self) -> dict[str, Any] | None:
|
||||
"""Parse OR expressions"""
|
||||
left = self._parse_and_expression()
|
||||
|
||||
while self.current_token.type == TokenType.OR:
|
||||
self._consume(TokenType.OR)
|
||||
right = self._parse_and_expression()
|
||||
left = {"type": "logical", "operator": "or", "left": left, "right": right}
|
||||
|
||||
return left
|
||||
|
||||
def _parse_and_expression(self) -> dict[str, Any] | None:
|
||||
"""Parse AND expressions"""
|
||||
left = self._parse_primary_expression()
|
||||
|
||||
while self.current_token.type == TokenType.AND:
|
||||
self._consume(TokenType.AND)
|
||||
right = self._parse_primary_expression()
|
||||
left = {"type": "logical", "operator": "and", "left": left, "right": right}
|
||||
|
||||
return left
|
||||
|
||||
def _parse_primary_expression(self) -> dict[str, Any] | None:
|
||||
"""Parse primary expressions (attribute operator value)"""
|
||||
if self.current_token.type == TokenType.LPAREN:
|
||||
self._consume(TokenType.LPAREN)
|
||||
expr = self._parse_or_expression()
|
||||
self._consume(TokenType.RPAREN)
|
||||
return expr
|
||||
|
||||
if self.current_token.type == TokenType.NOT:
|
||||
self._consume(TokenType.NOT)
|
||||
expr = self._parse_primary_expression()
|
||||
return {"type": "logical", "operator": "not", "operand": expr}
|
||||
|
||||
if self.current_token.type != TokenType.ATTRIBUTE:
|
||||
return None
|
||||
|
||||
attribute = self.current_token.value
|
||||
self._consume(TokenType.ATTRIBUTE)
|
||||
|
||||
if self.current_token.type != TokenType.OPERATOR:
|
||||
return None
|
||||
|
||||
operator = self.current_token.value
|
||||
self._consume(TokenType.OPERATOR)
|
||||
|
||||
# Parse value
|
||||
value = None
|
||||
if self.current_token.type == TokenType.STRING:
|
||||
value = self.current_token.value
|
||||
self._consume(TokenType.STRING)
|
||||
elif self.current_token.type == TokenType.NUMBER:
|
||||
value = (
|
||||
float(self.current_token.value)
|
||||
if "." in self.current_token.value
|
||||
else int(self.current_token.value)
|
||||
)
|
||||
self._consume(TokenType.NUMBER)
|
||||
elif self.current_token.type == TokenType.BOOLEAN:
|
||||
value = self.current_token.value
|
||||
self._consume(TokenType.BOOLEAN)
|
||||
elif self.current_token.type == TokenType.NULL:
|
||||
value = None
|
||||
self._consume(TokenType.NULL)
|
||||
|
||||
return {"type": "comparison", "attribute": attribute, "operator": operator, "value": value}
|
||||
|
||||
def _consume(self, expected_type: TokenType):
|
||||
"""Consume a token of the expected type"""
|
||||
if self.current_token.type == expected_type:
|
||||
self.current_token = self.lexer.get_next_token()
|
||||
else:
|
||||
raise ValueError(f"Expected {expected_type}, got {self.current_token.type}")
|
||||
246
authentik/sources/scim/patch/processor.py
Normal file
246
authentik/sources/scim/patch/processor.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from typing import Any
|
||||
|
||||
from authentik.providers.scim.clients.schema import PatchOp, PatchOperation
|
||||
from authentik.sources.scim.constants import SCIM_URN_USER_ENTERPRISE
|
||||
from authentik.sources.scim.patch.parser import SCIMPathParser
|
||||
|
||||
|
||||
class SCIMPatchProcessor:
|
||||
"""Processes SCIM patch operations on Python dictionaries"""
|
||||
|
||||
def __init__(self):
|
||||
self.parser = SCIMPathParser()
|
||||
|
||||
def apply_patches(self, data: dict[str, Any], patches: list[PatchOperation]) -> dict[str, Any]:
|
||||
"""Apply a list of patch operations to the data"""
|
||||
result = data.copy()
|
||||
|
||||
for _patch in patches:
|
||||
patch = PatchOperation.model_validate(_patch)
|
||||
if patch.path is None:
|
||||
# Handle operations with no path - value contains attribute paths as keys
|
||||
self._apply_bulk_operation(result, patch.op, patch.value)
|
||||
elif patch.op == PatchOp.add:
|
||||
self._apply_add(result, patch.path, patch.value)
|
||||
elif patch.op == PatchOp.remove:
|
||||
self._apply_remove(result, patch.path)
|
||||
elif patch.op == PatchOp.replace:
|
||||
self._apply_replace(result, patch.path, patch.value)
|
||||
|
||||
return result
|
||||
|
||||
def _apply_bulk_operation(
|
||||
self, data: dict[str, Any], operation: PatchOp, value: dict[str, Any]
|
||||
):
|
||||
"""Apply bulk operations when path is None"""
|
||||
if not isinstance(value, dict):
|
||||
return
|
||||
for path, val in value.items():
|
||||
if operation == PatchOp.add:
|
||||
self._apply_add(data, path, val)
|
||||
elif operation == PatchOp.remove:
|
||||
self._apply_remove(data, path)
|
||||
elif operation == PatchOp.replace:
|
||||
self._apply_replace(data, path, val)
|
||||
|
||||
def _apply_add(self, data: dict[str, Any], path: str, value: Any):
|
||||
"""Apply ADD operation"""
|
||||
components = self.parser.parse_path(path)
|
||||
|
||||
if len(components) == 1 and not components[0]["filter"]:
|
||||
# Simple path
|
||||
attr = components[0]["attribute"]
|
||||
if components[0]["sub_attribute"]:
|
||||
if attr not in data:
|
||||
data[attr] = {}
|
||||
# Somewhat hacky workaround for the manager attribute of the enterprise schema
|
||||
# ideally we'd do this based on the schema
|
||||
if attr == SCIM_URN_USER_ENTERPRISE and components[0]["sub_attribute"] == "manager":
|
||||
data[attr][components[0]["sub_attribute"]] = {"value": value}
|
||||
else:
|
||||
data[attr][components[0]["sub_attribute"]] = value
|
||||
elif attr in data:
|
||||
data[attr].append(value)
|
||||
else:
|
||||
data[attr] = value
|
||||
else:
|
||||
# Complex path with filters
|
||||
self._navigate_and_modify(data, components, value, "add")
|
||||
|
||||
def _apply_remove(self, data: dict[str, Any], path: str):
|
||||
"""Apply REMOVE operation"""
|
||||
components = self.parser.parse_path(path)
|
||||
|
||||
if len(components) == 1 and not components[0]["filter"]:
|
||||
# Simple path
|
||||
attr = components[0]["attribute"]
|
||||
if components[0]["sub_attribute"]:
|
||||
if attr in data and isinstance(data[attr], dict):
|
||||
data[attr].pop(components[0]["sub_attribute"], None)
|
||||
else:
|
||||
data.pop(attr, None)
|
||||
else:
|
||||
# Complex path with filters
|
||||
self._navigate_and_modify(data, components, None, "remove")
|
||||
|
||||
def _apply_replace(self, data: dict[str, Any], path: str, value: Any):
|
||||
"""Apply REPLACE operation"""
|
||||
components = self.parser.parse_path(path)
|
||||
|
||||
if len(components) == 1 and not components[0]["filter"]:
|
||||
# Simple path
|
||||
attr = components[0]["attribute"]
|
||||
if components[0]["sub_attribute"]:
|
||||
if attr not in data:
|
||||
data[attr] = {}
|
||||
# Somewhat hacky workaround for the manager attribute of the enterprise schema
|
||||
# ideally we'd do this based on the schema
|
||||
if attr == SCIM_URN_USER_ENTERPRISE and components[0]["sub_attribute"] == "manager":
|
||||
data[attr][components[0]["sub_attribute"]] = {"value": value}
|
||||
else:
|
||||
data[attr][components[0]["sub_attribute"]] = value
|
||||
else:
|
||||
data[attr] = value
|
||||
else:
|
||||
# Complex path with filters
|
||||
self._navigate_and_modify(data, components, value, "replace")
|
||||
|
||||
def _navigate_and_modify( # noqa PLR0912
|
||||
self, data: dict[str, Any], components: list[dict[str, Any]], value: Any, operation: str
|
||||
):
|
||||
"""Navigate through complex paths and apply modifications"""
|
||||
current = data
|
||||
|
||||
for i, component in enumerate(components):
|
||||
attr = component["attribute"]
|
||||
filter_expr = component["filter"]
|
||||
sub_attr = component["sub_attribute"]
|
||||
|
||||
if filter_expr:
|
||||
# Handle array with filter
|
||||
if attr not in current:
|
||||
if operation == "add":
|
||||
current[attr] = []
|
||||
else:
|
||||
return
|
||||
|
||||
if not isinstance(current[attr], list):
|
||||
return
|
||||
|
||||
# Find matching items
|
||||
matching_items = []
|
||||
for item in current[attr]:
|
||||
if self._matches_filter(item, filter_expr):
|
||||
matching_items.append(item)
|
||||
|
||||
if not matching_items and operation == "add":
|
||||
# Create new item if none match (only for simple comparison filters)
|
||||
if filter_expr.get("type", "comparison") == "comparison":
|
||||
new_item = {filter_expr["attribute"]: filter_expr["value"]}
|
||||
current[attr].append(new_item)
|
||||
matching_items = [new_item]
|
||||
|
||||
# Apply operation to matching items
|
||||
for item in matching_items:
|
||||
if sub_attr:
|
||||
if operation in {"add", "replace"}:
|
||||
item[sub_attr] = value
|
||||
elif operation == "remove":
|
||||
item.pop(sub_attr, None)
|
||||
elif operation in {"add", "replace"}:
|
||||
if isinstance(value, dict):
|
||||
item.update(value)
|
||||
else:
|
||||
# If value is not a dict, we can't merge it
|
||||
pass
|
||||
elif operation == "remove":
|
||||
# Remove the entire item
|
||||
if item in current[attr]:
|
||||
current[attr].remove(item)
|
||||
# Handle simple attribute
|
||||
elif i == len(components) - 1:
|
||||
# Last component
|
||||
if sub_attr:
|
||||
if attr not in current:
|
||||
current[attr] = {}
|
||||
if operation in {"add", "replace"}:
|
||||
current[attr][sub_attr] = value
|
||||
elif operation == "remove":
|
||||
current[attr].pop(sub_attr, None)
|
||||
elif operation in {"add", "replace"}:
|
||||
current[attr] = value
|
||||
elif operation == "remove":
|
||||
current.pop(attr, None)
|
||||
else:
|
||||
# Navigate deeper
|
||||
if attr not in current:
|
||||
current[attr] = {}
|
||||
current = current[attr]
|
||||
|
||||
def _matches_filter(self, item: dict[str, Any], filter_expr: dict[str, Any]) -> bool:
|
||||
"""Check if an item matches the filter expression"""
|
||||
if not filter_expr:
|
||||
return True
|
||||
|
||||
filter_type = filter_expr.get("type", "comparison")
|
||||
|
||||
if filter_type == "comparison":
|
||||
return self._matches_comparison(item, filter_expr)
|
||||
elif filter_type == "logical":
|
||||
return self._matches_logical(item, filter_expr)
|
||||
|
||||
return False
|
||||
|
||||
def _matches_comparison( # noqa PLR0912
|
||||
self, item: dict[str, Any], filter_expr: dict[str, Any]
|
||||
) -> bool:
|
||||
"""Check if an item matches a comparison filter"""
|
||||
attr = filter_expr["attribute"]
|
||||
operator = filter_expr["operator"]
|
||||
expected_value = filter_expr["value"]
|
||||
|
||||
if attr not in item:
|
||||
return False
|
||||
|
||||
actual_value = item[attr]
|
||||
|
||||
if operator == "eq":
|
||||
return actual_value == expected_value
|
||||
elif operator == "ne":
|
||||
return actual_value != expected_value
|
||||
elif operator == "co":
|
||||
return str(expected_value) in str(actual_value)
|
||||
elif operator == "sw":
|
||||
return str(actual_value).startswith(str(expected_value))
|
||||
elif operator == "ew":
|
||||
return str(actual_value).endswith(str(expected_value))
|
||||
elif operator == "gt":
|
||||
return actual_value > expected_value
|
||||
elif operator == "lt":
|
||||
return actual_value < expected_value
|
||||
elif operator == "ge":
|
||||
return actual_value >= expected_value
|
||||
elif operator == "le":
|
||||
return actual_value <= expected_value
|
||||
elif operator == "pr":
|
||||
return actual_value is not None
|
||||
|
||||
return False
|
||||
|
||||
def _matches_logical(self, item: dict[str, Any], filter_expr: dict[str, Any]) -> bool:
|
||||
"""Check if an item matches a logical filter expression"""
|
||||
operator = filter_expr["operator"]
|
||||
|
||||
if operator == "and":
|
||||
left_result = self._matches_filter(item, filter_expr["left"])
|
||||
right_result = self._matches_filter(item, filter_expr["right"])
|
||||
return left_result and right_result
|
||||
elif operator == "or":
|
||||
left_result = self._matches_filter(item, filter_expr["left"])
|
||||
right_result = self._matches_filter(item, filter_expr["right"])
|
||||
return left_result or right_result
|
||||
elif operator == "not":
|
||||
operand_result = self._matches_filter(item, filter_expr["operand"])
|
||||
return not operand_result
|
||||
|
||||
return False
|
||||
@@ -1101,17 +1101,6 @@
|
||||
"returned": "default",
|
||||
"uniqueness": "none"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"multiValued": false,
|
||||
"description": "The User's cleartext password. This attribute is intended to be used as a means to specify an initial\npassword when creating a new User or to reset an existing User's password.",
|
||||
"required": false,
|
||||
"caseExact": false,
|
||||
"mutability": "writeOnly",
|
||||
"returned": "never",
|
||||
"uniqueness": "none"
|
||||
},
|
||||
{
|
||||
"name": "emails",
|
||||
"type": "complex",
|
||||
|
||||
@@ -75,7 +75,9 @@ class TestSCIMGroups(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||
self.assertTrue(
|
||||
SCIMSourceGroup.objects.filter(source=self.source, external_id=ext_id).exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||
@@ -86,6 +88,7 @@ class TestSCIMGroups(APITestCase):
|
||||
"""Test group create"""
|
||||
user = create_test_user()
|
||||
ext_id = generate_id()
|
||||
name = generate_id()
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
@@ -95,7 +98,7 @@ class TestSCIMGroups(APITestCase):
|
||||
),
|
||||
data=dumps(
|
||||
{
|
||||
"displayName": generate_id(),
|
||||
"displayName": name,
|
||||
"externalId": ext_id,
|
||||
"members": [{"value": str(user.uuid)}],
|
||||
}
|
||||
@@ -104,12 +107,22 @@ class TestSCIMGroups(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||
connection = SCIMSourceGroup.objects.filter(source=self.source, external_id=ext_id).first()
|
||||
self.assertIsNotNone(connection)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||
).exists()
|
||||
)
|
||||
connection.refresh_from_db()
|
||||
self.assertEqual(
|
||||
connection.attributes,
|
||||
{
|
||||
"displayName": name,
|
||||
"externalId": ext_id,
|
||||
"members": [{"value": str(user.uuid)}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_group_create_members_empty(self):
|
||||
"""Test group create"""
|
||||
@@ -126,7 +139,9 @@ class TestSCIMGroups(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||
self.assertTrue(
|
||||
SCIMSourceGroup.objects.filter(source=self.source, external_id=ext_id).exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||
@@ -136,7 +151,9 @@ class TestSCIMGroups(APITestCase):
|
||||
def test_group_create_duplicate(self):
|
||||
"""Test group create (duplicate)"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||
existing = SCIMSourceGroup.objects.create(
|
||||
source=self.source, group=group, external_id=uuid4()
|
||||
)
|
||||
ext_id = generate_id()
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
@@ -165,7 +182,9 @@ class TestSCIMGroups(APITestCase):
|
||||
def test_group_update(self):
|
||||
"""Test group update"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||
existing = SCIMSourceGroup.objects.create(
|
||||
source=self.source, group=group, external_id=uuid4()
|
||||
)
|
||||
ext_id = generate_id()
|
||||
response = self.client.put(
|
||||
reverse(
|
||||
@@ -205,12 +224,49 @@ class TestSCIMGroups(APITestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_group_patch_add(self):
|
||||
def test_group_patch_modify(self):
|
||||
"""Test group patch"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
connection = SCIMSourceGroup.objects.create(
|
||||
source=self.source,
|
||||
group=group,
|
||||
external_id=uuid4(),
|
||||
attributes={"displayName": group.name, "members": []},
|
||||
)
|
||||
response = self.client.patch(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
kwargs={"source_slug": self.source.slug, "group_id": group.pk},
|
||||
),
|
||||
data=dumps(
|
||||
{
|
||||
"Operations": [
|
||||
{
|
||||
"op": "Add",
|
||||
"value": {"externalId": "d85051cb-0557-4aa1-98ca-51eabcee4d40"},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
connection = SCIMSourceGroup.objects.filter(id="d85051cb-0557-4aa1-98ca-51eabcee4d40")
|
||||
self.assertIsNotNone(connection)
|
||||
|
||||
def test_group_patch_member_add(self):
|
||||
"""Test group patch"""
|
||||
user = create_test_user()
|
||||
|
||||
other_user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||
group.users.add(other_user)
|
||||
connection = SCIMSourceGroup.objects.create(
|
||||
source=self.source,
|
||||
group=group,
|
||||
external_id=uuid4(),
|
||||
attributes={"displayName": group.name, "members": [{"value": str(other_user.uuid)}]},
|
||||
)
|
||||
response = self.client.patch(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
@@ -222,7 +278,7 @@ class TestSCIMGroups(APITestCase):
|
||||
{
|
||||
"op": "Add",
|
||||
"path": "members",
|
||||
"value": {"value": str(user.uuid)},
|
||||
"value": [{"value": str(user.uuid)}],
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -230,16 +286,33 @@ class TestSCIMGroups(APITestCase):
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, second=200)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertTrue(group.users.filter(pk=user.pk).exists())
|
||||
self.assertTrue(group.users.filter(pk=other_user.pk).exists())
|
||||
connection.refresh_from_db()
|
||||
self.assertEqual(
|
||||
connection.attributes,
|
||||
{
|
||||
"displayName": group.name,
|
||||
"members": sorted(
|
||||
[{"value": str(other_user.uuid)}, {"value": str(user.uuid)}],
|
||||
key=lambda u: u["value"],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def test_group_patch_remove(self):
|
||||
def test_group_patch_member_remove(self):
|
||||
"""Test group patch"""
|
||||
user = create_test_user()
|
||||
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.users.add(user)
|
||||
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||
connection = SCIMSourceGroup.objects.create(
|
||||
source=self.source,
|
||||
group=group,
|
||||
external_id=uuid4(),
|
||||
attributes={"displayName": group.name, "members": []},
|
||||
)
|
||||
response = self.client.patch(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
@@ -251,7 +324,7 @@ class TestSCIMGroups(APITestCase):
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "members",
|
||||
"value": {"value": str(user.uuid)},
|
||||
"value": [{"value": str(user.uuid)}],
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -259,13 +332,21 @@ class TestSCIMGroups(APITestCase):
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, second=200)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertFalse(group.users.filter(pk=user.pk).exists())
|
||||
connection.refresh_from_db()
|
||||
self.assertEqual(
|
||||
connection.attributes,
|
||||
{
|
||||
"displayName": group.name,
|
||||
"members": [],
|
||||
},
|
||||
)
|
||||
|
||||
def test_group_delete(self):
|
||||
"""Test group delete"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||
SCIMSourceGroup.objects.create(source=self.source, group=group, external_id=uuid4())
|
||||
response = self.client.delete(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
|
||||
510
authentik/sources/scim/tests/test_lexer.py
Normal file
510
authentik/sources/scim/tests/test_lexer.py
Normal file
@@ -0,0 +1,510 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from authentik.sources.scim.constants import (
|
||||
SCIM_URN_GROUP,
|
||||
SCIM_URN_SCHEMA,
|
||||
SCIM_URN_USER,
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
)
|
||||
from authentik.sources.scim.patch.lexer import SCIMPathLexer, Token, TokenType
|
||||
|
||||
|
||||
class TestTokenType(TestCase):
|
||||
"""Test TokenType enum"""
|
||||
|
||||
def test_token_type_values(self):
|
||||
"""Test that all token types have correct values"""
|
||||
self.assertEqual(TokenType.ATTRIBUTE.value, "ATTRIBUTE")
|
||||
self.assertEqual(TokenType.DOT.value, "DOT")
|
||||
self.assertEqual(TokenType.LBRACKET.value, "LBRACKET")
|
||||
self.assertEqual(TokenType.RBRACKET.value, "RBRACKET")
|
||||
self.assertEqual(TokenType.LPAREN.value, "LPAREN")
|
||||
self.assertEqual(TokenType.RPAREN.value, "RPAREN")
|
||||
self.assertEqual(TokenType.STRING.value, "STRING")
|
||||
self.assertEqual(TokenType.NUMBER.value, "NUMBER")
|
||||
self.assertEqual(TokenType.BOOLEAN.value, "BOOLEAN")
|
||||
self.assertEqual(TokenType.NULL.value, "NULL")
|
||||
self.assertEqual(TokenType.OPERATOR.value, "OPERATOR")
|
||||
self.assertEqual(TokenType.AND.value, "AND")
|
||||
self.assertEqual(TokenType.OR.value, "OR")
|
||||
self.assertEqual(TokenType.NOT.value, "NOT")
|
||||
self.assertEqual(TokenType.EOF.value, "EOF")
|
||||
|
||||
|
||||
class TestToken(TestCase):
|
||||
"""Test Token dataclass"""
|
||||
|
||||
def test_token_creation(self):
|
||||
"""Test token creation with all parameters"""
|
||||
token = Token(TokenType.ATTRIBUTE, "userName", 5)
|
||||
self.assertEqual(token.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token.value, "userName")
|
||||
self.assertEqual(token.position, 5)
|
||||
|
||||
def test_token_creation_default_position(self):
|
||||
"""Test token creation with default position"""
|
||||
token = Token(TokenType.DOT, ".")
|
||||
self.assertEqual(token.type, TokenType.DOT)
|
||||
self.assertEqual(token.value, ".")
|
||||
self.assertEqual(token.position, 0)
|
||||
|
||||
|
||||
class TestSCIMPathLexer(TestCase):
|
||||
"""Test SCIMPathLexer class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.simple_lexer = SCIMPathLexer("userName")
|
||||
|
||||
def test_init(self):
|
||||
"""Test lexer initialization"""
|
||||
lexer = SCIMPathLexer("test")
|
||||
self.assertEqual(lexer.text, "test")
|
||||
self.assertEqual(lexer.pos, 0)
|
||||
self.assertEqual(lexer.current_char, "t")
|
||||
self.assertIn(SCIM_URN_SCHEMA, lexer.schema_urns)
|
||||
self.assertIn(SCIM_URN_GROUP, lexer.schema_urns)
|
||||
self.assertIn(SCIM_URN_USER, lexer.schema_urns)
|
||||
self.assertIn(SCIM_URN_USER_ENTERPRISE, lexer.schema_urns)
|
||||
self.assertEqual(
|
||||
lexer.OPERATORS, ["eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le", "pr"]
|
||||
)
|
||||
|
||||
def test_init_empty_string(self):
|
||||
"""Test lexer initialization with empty string"""
|
||||
lexer = SCIMPathLexer("")
|
||||
self.assertEqual(lexer.text, "")
|
||||
self.assertEqual(lexer.pos, 0)
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_advance(self):
|
||||
"""Test advance method"""
|
||||
lexer = SCIMPathLexer("abc")
|
||||
self.assertEqual(lexer.current_char, "a")
|
||||
|
||||
lexer.advance()
|
||||
self.assertEqual(lexer.pos, 1)
|
||||
self.assertEqual(lexer.current_char, "b")
|
||||
|
||||
lexer.advance()
|
||||
self.assertEqual(lexer.pos, 2)
|
||||
self.assertEqual(lexer.current_char, "c")
|
||||
|
||||
lexer.advance()
|
||||
self.assertEqual(lexer.pos, 3)
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_skip_whitespace(self):
|
||||
"""Test skip_whitespace method"""
|
||||
lexer = SCIMPathLexer(" \t\n abc")
|
||||
lexer.skip_whitespace()
|
||||
self.assertEqual(lexer.current_char, "a")
|
||||
|
||||
def test_skip_whitespace_only_whitespace(self):
|
||||
"""Test skip_whitespace with only whitespace"""
|
||||
lexer = SCIMPathLexer(" \t\n ")
|
||||
lexer.skip_whitespace()
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_skip_whitespace_no_whitespace(self):
|
||||
"""Test skip_whitespace with no leading whitespace"""
|
||||
lexer = SCIMPathLexer("abc")
|
||||
original_pos = lexer.pos
|
||||
lexer.skip_whitespace()
|
||||
self.assertEqual(lexer.pos, original_pos)
|
||||
self.assertEqual(lexer.current_char, "a")
|
||||
|
||||
def test_read_string_double_quotes(self):
|
||||
"""Test reading double-quoted string"""
|
||||
lexer = SCIMPathLexer('"hello world"')
|
||||
result = lexer.read_string('"')
|
||||
self.assertEqual(result, "hello world")
|
||||
self.assertIsNone(lexer.current_char) # Should be at end
|
||||
|
||||
def test_read_string_single_quotes(self):
|
||||
"""Test reading single-quoted string"""
|
||||
lexer = SCIMPathLexer("'hello world'")
|
||||
result = lexer.read_string("'")
|
||||
self.assertEqual(result, "hello world")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_string_with_escapes(self):
|
||||
"""Test reading string with escape characters"""
|
||||
lexer = SCIMPathLexer('"hello \\"world\\""')
|
||||
result = lexer.read_string('"')
|
||||
self.assertEqual(result, 'hello "world"')
|
||||
|
||||
def test_read_string_with_backslash_at_end(self):
|
||||
"""Test reading string with backslash at end"""
|
||||
lexer = SCIMPathLexer('"hello\\"')
|
||||
result = lexer.read_string('"')
|
||||
self.assertEqual(result, 'hello"')
|
||||
|
||||
def test_read_string_unclosed(self):
|
||||
"""Test reading unclosed string"""
|
||||
lexer = SCIMPathLexer('"hello world')
|
||||
result = lexer.read_string('"')
|
||||
self.assertEqual(result, "hello world")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_string_empty(self):
|
||||
"""Test reading empty string"""
|
||||
lexer = SCIMPathLexer('""')
|
||||
result = lexer.read_string('"')
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_read_number_integer(self):
|
||||
"""Test reading integer number"""
|
||||
lexer = SCIMPathLexer("123")
|
||||
result = lexer.read_number()
|
||||
self.assertEqual(result, "123")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_number_float(self):
|
||||
"""Test reading float number"""
|
||||
lexer = SCIMPathLexer("123.456")
|
||||
result = lexer.read_number()
|
||||
self.assertEqual(result, "123.456")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_number_with_multiple_dots(self):
|
||||
"""Test reading number with multiple dots (invalid but handled)"""
|
||||
lexer = SCIMPathLexer("123.456.789")
|
||||
result = lexer.read_number()
|
||||
self.assertEqual(result, "123.456.789")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_number_starting_with_dot(self):
|
||||
"""Test reading number starting with dot"""
|
||||
lexer = SCIMPathLexer(".123")
|
||||
result = lexer.read_number()
|
||||
self.assertEqual(result, ".123")
|
||||
|
||||
def test_read_identifier_simple(self):
|
||||
"""Test reading simple identifier"""
|
||||
lexer = SCIMPathLexer("userName")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "userName")
|
||||
self.assertIsNone(lexer.current_char)
|
||||
|
||||
def test_read_identifier_with_underscore(self):
|
||||
"""Test reading identifier with underscore"""
|
||||
lexer = SCIMPathLexer("user_name")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "user_name")
|
||||
|
||||
def test_read_identifier_with_hyphen(self):
|
||||
"""Test reading identifier with hyphen"""
|
||||
lexer = SCIMPathLexer("user-name")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "user-name")
|
||||
|
||||
def test_read_identifier_with_colon(self):
|
||||
"""Test reading identifier with colon (URN format)"""
|
||||
lexer = SCIMPathLexer("urn:ietf:params:scim:schemas:core:2.0:User")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "urn:ietf:params:scim:schemas:core:2.0:User")
|
||||
|
||||
def test_read_identifier_schema_urn(self):
|
||||
"""Test reading schema URN identifier"""
|
||||
lexer = SCIMPathLexer(f"{SCIM_URN_USER}.userName")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, SCIM_URN_USER)
|
||||
self.assertEqual(lexer.current_char, ".") # Should stop at dot and set current_char to dot
|
||||
|
||||
def test_read_identifier_with_version_number(self):
|
||||
"""Test reading identifier with version number (dots followed by digits)"""
|
||||
lexer = SCIMPathLexer("urn:ietf:params:scim:schemas:core:2.0:User")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "urn:ietf:params:scim:schemas:core:2.0:User")
|
||||
|
||||
def test_read_identifier_partial_urn_match(self):
|
||||
"""Test reading identifier that partially matches URN"""
|
||||
lexer = SCIMPathLexer("urn:ietf:params:scim:schemas:core:2.0:CustomUser")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "urn:ietf:params:scim:schemas:core:2.0:CustomUser")
|
||||
|
||||
# Test get_next_token method
|
||||
def test_get_next_token_dot(self):
|
||||
"""Test tokenizing dot"""
|
||||
lexer = SCIMPathLexer(".")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.DOT)
|
||||
self.assertEqual(token.value, ".")
|
||||
|
||||
def test_get_next_token_lbracket(self):
|
||||
"""Test tokenizing left bracket"""
|
||||
lexer = SCIMPathLexer("[")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.LBRACKET)
|
||||
self.assertEqual(token.value, "[")
|
||||
|
||||
def test_get_next_token_rbracket(self):
|
||||
"""Test tokenizing right bracket"""
|
||||
lexer = SCIMPathLexer("]")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.RBRACKET)
|
||||
self.assertEqual(token.value, "]")
|
||||
|
||||
def test_get_next_token_lparen(self):
|
||||
"""Test tokenizing left parenthesis"""
|
||||
lexer = SCIMPathLexer("(")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.LPAREN)
|
||||
self.assertEqual(token.value, "(")
|
||||
|
||||
def test_get_next_token_rparen(self):
|
||||
"""Test tokenizing right parenthesis"""
|
||||
lexer = SCIMPathLexer(")")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.RPAREN)
|
||||
self.assertEqual(token.value, ")")
|
||||
|
||||
def test_get_next_token_string_double_quotes(self):
|
||||
"""Test tokenizing double-quoted string"""
|
||||
lexer = SCIMPathLexer('"test string"')
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.STRING)
|
||||
self.assertEqual(token.value, "test string")
|
||||
|
||||
def test_get_next_token_string_single_quotes(self):
|
||||
"""Test tokenizing single-quoted string"""
|
||||
lexer = SCIMPathLexer("'test string'")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.STRING)
|
||||
self.assertEqual(token.value, "test string")
|
||||
|
||||
def test_get_next_token_number_integer(self):
|
||||
"""Test tokenizing integer"""
|
||||
lexer = SCIMPathLexer("123")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.NUMBER)
|
||||
self.assertEqual(token.value, "123")
|
||||
|
||||
def test_get_next_token_number_float(self):
|
||||
"""Test tokenizing float"""
|
||||
lexer = SCIMPathLexer("123.45")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.NUMBER)
|
||||
self.assertEqual(token.value, "123.45")
|
||||
|
||||
def test_get_next_token_boolean_true(self):
|
||||
"""Test tokenizing boolean true"""
|
||||
lexer = SCIMPathLexer("true")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.BOOLEAN)
|
||||
self.assertTrue(token.value)
|
||||
|
||||
def test_get_next_token_boolean_false(self):
|
||||
"""Test tokenizing boolean false"""
|
||||
lexer = SCIMPathLexer("false")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.BOOLEAN)
|
||||
self.assertFalse(token.value)
|
||||
|
||||
def test_get_next_token_boolean_case_insensitive(self):
|
||||
"""Test tokenizing boolean with different cases"""
|
||||
for value in ["TRUE", "True", "FALSE", "False"]:
|
||||
with self.subTest(value=value):
|
||||
lexer = SCIMPathLexer(value)
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.BOOLEAN)
|
||||
|
||||
def test_get_next_token_null(self):
|
||||
"""Test tokenizing null"""
|
||||
lexer = SCIMPathLexer("null")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.NULL)
|
||||
self.assertIsNone(token.value)
|
||||
|
||||
def test_get_next_token_null_case_insensitive(self):
|
||||
"""Test tokenizing null with different cases"""
|
||||
for value in ["NULL", "Null"]:
|
||||
with self.subTest(value=value):
|
||||
lexer = SCIMPathLexer(value)
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.NULL)
|
||||
|
||||
def test_get_next_token_and(self):
|
||||
"""Test tokenizing AND operator"""
|
||||
lexer = SCIMPathLexer("and")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.AND)
|
||||
self.assertEqual(token.value, "and")
|
||||
|
||||
def test_get_next_token_or(self):
|
||||
"""Test tokenizing OR operator"""
|
||||
lexer = SCIMPathLexer("or")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.OR)
|
||||
self.assertEqual(token.value, "or")
|
||||
|
||||
def test_get_next_token_not(self):
|
||||
"""Test tokenizing NOT operator"""
|
||||
lexer = SCIMPathLexer("not")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.NOT)
|
||||
self.assertEqual(token.value, "not")
|
||||
|
||||
def test_get_next_token_operators(self):
|
||||
"""Test tokenizing all comparison operators"""
|
||||
operators = ["eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le", "pr"]
|
||||
for op in operators:
|
||||
with self.subTest(operator=op):
|
||||
lexer = SCIMPathLexer(op)
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.OPERATOR)
|
||||
self.assertEqual(token.value, op)
|
||||
|
||||
def test_get_next_token_operators_case_insensitive(self):
|
||||
"""Test tokenizing operators with different cases"""
|
||||
for op in ["EQ", "Eq", "NE", "Ne"]:
|
||||
with self.subTest(operator=op):
|
||||
lexer = SCIMPathLexer(op)
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.OPERATOR)
|
||||
self.assertEqual(token.value, op.lower())
|
||||
|
||||
def test_get_next_token_attribute(self):
|
||||
"""Test tokenizing attribute name"""
|
||||
lexer = SCIMPathLexer("userName")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token.value, "userName")
|
||||
|
||||
def test_get_next_token_attribute_with_underscore(self):
|
||||
"""Test tokenizing attribute name with underscore"""
|
||||
lexer = SCIMPathLexer("_userName")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token.value, "_userName")
|
||||
|
||||
def test_get_next_token_eof(self):
|
||||
"""Test tokenizing end of file"""
|
||||
lexer = SCIMPathLexer("")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.EOF)
|
||||
self.assertEqual(token.value, "")
|
||||
|
||||
def test_get_next_token_with_whitespace(self):
|
||||
"""Test tokenizing with leading whitespace"""
|
||||
lexer = SCIMPathLexer(" userName")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token.value, "userName")
|
||||
|
||||
def test_get_next_token_skip_unknown_characters(self):
|
||||
"""Test that unknown characters are skipped"""
|
||||
lexer = SCIMPathLexer("@#$userName")
|
||||
token = lexer.get_next_token()
|
||||
self.assertEqual(token.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token.value, "userName")
|
||||
|
||||
def test_get_next_token_multiple_tokens(self):
|
||||
"""Test tokenizing multiple tokens in sequence"""
|
||||
lexer = SCIMPathLexer("userName.givenName")
|
||||
|
||||
token1 = lexer.get_next_token()
|
||||
self.assertEqual(token1.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token1.value, "userName")
|
||||
|
||||
token2 = lexer.get_next_token()
|
||||
self.assertEqual(token2.type, TokenType.DOT)
|
||||
self.assertEqual(token2.value, ".")
|
||||
|
||||
token3 = lexer.get_next_token()
|
||||
self.assertEqual(token3.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token3.value, "givenName")
|
||||
|
||||
token4 = lexer.get_next_token()
|
||||
self.assertEqual(token4.type, TokenType.EOF)
|
||||
|
||||
def test_get_next_token_complex_filter(self):
|
||||
"""Test tokenizing complex filter expression"""
|
||||
lexer = SCIMPathLexer('emails[type eq "work" and primary eq true]')
|
||||
|
||||
tokens = []
|
||||
while True:
|
||||
token = lexer.get_next_token()
|
||||
tokens.append(token)
|
||||
if token.type == TokenType.EOF:
|
||||
break
|
||||
|
||||
expected_types = [
|
||||
TokenType.ATTRIBUTE, # emails
|
||||
TokenType.LBRACKET, # [
|
||||
TokenType.ATTRIBUTE, # type
|
||||
TokenType.OPERATOR, # eq
|
||||
TokenType.STRING, # "work"
|
||||
TokenType.AND, # and
|
||||
TokenType.ATTRIBUTE, # primary
|
||||
TokenType.OPERATOR, # eq
|
||||
TokenType.BOOLEAN, # true
|
||||
TokenType.RBRACKET, # ]
|
||||
TokenType.EOF,
|
||||
]
|
||||
|
||||
self.assertEqual(len(tokens), len(expected_types))
|
||||
for token, expected_type in zip(tokens, expected_types, strict=False):
|
||||
self.assertEqual(token.type, expected_type)
|
||||
|
||||
def test_get_next_token_urn_attribute(self):
|
||||
"""Test tokenizing URN-based attribute"""
|
||||
lexer = SCIMPathLexer(f"{SCIM_URN_USER}.userName")
|
||||
|
||||
token1 = lexer.get_next_token()
|
||||
self.assertEqual(token1.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token1.value, SCIM_URN_USER)
|
||||
|
||||
token2 = lexer.get_next_token()
|
||||
self.assertEqual(token2.type, TokenType.DOT)
|
||||
|
||||
token3 = lexer.get_next_token()
|
||||
self.assertEqual(token3.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token3.value, "userName")
|
||||
|
||||
def test_get_next_token_enterprise_urn(self):
|
||||
"""Test tokenizing enterprise URN"""
|
||||
lexer = SCIMPathLexer(f"{SCIM_URN_USER_ENTERPRISE}.manager")
|
||||
|
||||
token1 = lexer.get_next_token()
|
||||
self.assertEqual(token1.type, TokenType.ATTRIBUTE)
|
||||
self.assertEqual(token1.value, SCIM_URN_USER_ENTERPRISE)
|
||||
|
||||
token2 = lexer.get_next_token()
|
||||
self.assertEqual(token2.type, TokenType.DOT)
|
||||
|
||||
def test_lexer_state_after_eof(self):
|
||||
"""Test lexer state after reaching EOF"""
|
||||
lexer = SCIMPathLexer("a")
|
||||
|
||||
# Get first token
|
||||
token1 = lexer.get_next_token()
|
||||
self.assertEqual(token1.type, TokenType.ATTRIBUTE)
|
||||
|
||||
# Get EOF token
|
||||
token2 = lexer.get_next_token()
|
||||
self.assertEqual(token2.type, TokenType.EOF)
|
||||
|
||||
# Should continue returning EOF
|
||||
token3 = lexer.get_next_token()
|
||||
self.assertEqual(token3.type, TokenType.EOF)
|
||||
|
||||
def test_read_identifier_edge_cases(self):
|
||||
"""Test read_identifier with edge cases"""
|
||||
# Test identifier ending with colon
|
||||
lexer = SCIMPathLexer("test:")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "test:")
|
||||
|
||||
# Test identifier with numbers
|
||||
lexer = SCIMPathLexer("test123")
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, "test123")
|
||||
|
||||
def test_complex_urn_parsing(self):
|
||||
"""Test parsing complex URN with version numbers"""
|
||||
urn = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
||||
lexer = SCIMPathLexer(urn)
|
||||
result = lexer.read_identifier()
|
||||
self.assertEqual(result, urn)
|
||||
1254
authentik/sources/scim/tests/test_patch.py
Normal file
1254
authentik/sources/scim/tests/test_patch.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ from authentik.core.tests.utils import create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
|
||||
from authentik.sources.scim.constants import SCIM_URN_USER_ENTERPRISE
|
||||
from authentik.sources.scim.models import SCIMSource, SCIMSourcePropertyMapping, SCIMSourceUser
|
||||
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE
|
||||
|
||||
@@ -81,7 +82,9 @@ class TestSCIMUsers(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(SCIMSourceUser.objects.filter(source=self.source, id=ext_id).exists())
|
||||
self.assertTrue(
|
||||
SCIMSourceUser.objects.filter(source=self.source, external_id=ext_id).exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||
@@ -174,14 +177,16 @@ class TestSCIMUsers(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(
|
||||
SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"],
|
||||
SCIMSourceUser.objects.get(source=self.source, external_id=ext_id).user.attributes[
|
||||
"phone"
|
||||
],
|
||||
"0123456789",
|
||||
)
|
||||
|
||||
def test_user_update(self):
|
||||
"""Test user update"""
|
||||
user = create_test_user()
|
||||
existing = SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4())
|
||||
existing = SCIMSourceUser.objects.create(source=self.source, user=user, external_id=uuid4())
|
||||
ext_id = generate_id()
|
||||
response = self.client.put(
|
||||
reverse(
|
||||
@@ -209,10 +214,51 @@ class TestSCIMUsers(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_user_update_patch(self):
|
||||
"""Test user update (patch)"""
|
||||
user = create_test_user()
|
||||
existing = SCIMSourceUser.objects.create(
|
||||
source=self.source,
|
||||
user=user,
|
||||
external_id=uuid4(),
|
||||
attributes={
|
||||
"userName": generate_id(),
|
||||
},
|
||||
)
|
||||
response = self.client.patch(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"user_id": str(user.uuid),
|
||||
},
|
||||
),
|
||||
data=dumps(
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "Add",
|
||||
"path": f"{SCIM_URN_USER_ENTERPRISE}:manager",
|
||||
"value": "86b2ed3e-30cd-4881-bb58-c4e910821339",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
existing.refresh_from_db()
|
||||
self.assertEqual(
|
||||
existing.attributes[SCIM_URN_USER_ENTERPRISE],
|
||||
{"manager": {"value": "86b2ed3e-30cd-4881-bb58-c4e910821339"}},
|
||||
)
|
||||
|
||||
def test_user_delete(self):
|
||||
"""Test user delete"""
|
||||
user = create_test_user()
|
||||
SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4())
|
||||
SCIMSourceUser.objects.create(source=self.source, user=user, external_id=uuid4())
|
||||
response = self.client.delete(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
|
||||
488
authentik/sources/scim/tests/test_users_patch.py
Normal file
488
authentik/sources/scim/tests/test_users_patch.py
Normal file
@@ -0,0 +1,488 @@
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.scim.constants import SCIM_URN_USER_ENTERPRISE
|
||||
from authentik.sources.scim.models import SCIMSource, SCIMSourceUser
|
||||
from authentik.sources.scim.patch.processor import SCIMPatchProcessor
|
||||
|
||||
|
||||
class TestSCIMUsersPatch(APITestCase):
|
||||
"""Test SCIM User Patch"""
|
||||
|
||||
def test_add(self):
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{"op": "Add", "path": "name.givenName", "value": "aqwer"},
|
||||
{"op": "Add", "path": "name.familyName", "value": "qwerqqqq"},
|
||||
{"op": "Add", "path": "name.formatted", "value": "aqwer qwerqqqq"},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"name": {
|
||||
"givenName": "aqwer",
|
||||
"familyName": "qwerqqqq",
|
||||
"formatted": "aqwer qwerqqqq",
|
||||
},
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
|
||||
def test_add_no_path(self):
|
||||
"""Test add patch with no path set"""
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{"op": "Add", "value": {"externalId": "aqwer"}},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "aqwer",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
|
||||
def test_replace(self):
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{"op": "Replace", "path": "name", "value": {"givenName": "aqwer"}},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"name": {
|
||||
"givenName": "aqwer",
|
||||
},
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
|
||||
def test_replace_no_path(self):
|
||||
"""Test value replace with no path"""
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{"op": "Replace", "value": {"externalId": "aqwer"}},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "aqwer",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
|
||||
def test_remove(self):
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{"op": "Remove", "path": "name", "value": {"givenName": "aqwer"}},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"name": {
|
||||
"givenName": "aqwer",
|
||||
},
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
|
||||
def test_large(self):
|
||||
"""Large amount of patch operations"""
|
||||
req = {
|
||||
"Operations": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "emails[primary eq true].value",
|
||||
"value": "dandre_kling@wintheiser.info",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "phoneNumbers[primary eq true].value",
|
||||
"value": "72-634-1548",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "phoneNumbers[primary eq true].display",
|
||||
"value": "72-634-1548",
|
||||
},
|
||||
{"op": "replace", "path": "ims[primary eq true].value", "value": "GXSGJKWGHVVS"},
|
||||
{"op": "replace", "path": "ims[primary eq true].display", "value": "IMCHDKUQIPYB"},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "photos[primary eq true].display",
|
||||
"value": "TWAWLHHSUNIV",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "addresses[primary eq true].formatted",
|
||||
"value": "TMINZQAJQDCL",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "addresses[primary eq true].streetAddress",
|
||||
"value": "081 Wisoky Key",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "addresses[primary eq true].locality",
|
||||
"value": "DPFASBZRPMDP",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "addresses[primary eq true].region",
|
||||
"value": "WHSTJSPIPTCF",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "addresses[primary eq true].postalCode",
|
||||
"value": "ko28 1qa",
|
||||
},
|
||||
{"op": "replace", "path": "addresses[primary eq true].country", "value": "Taiwan"},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "entitlements[primary eq true].value",
|
||||
"value": "NGBJMUYZVVBX",
|
||||
},
|
||||
{"op": "replace", "path": "roles[primary eq true].value", "value": "XEELVFMMWCVM"},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "x509Certificates[primary eq true].value",
|
||||
"value": "UYISMEDOXUZY",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"value": {
|
||||
"externalId": "7faaefb0-0774-4d8e-8f6d-863c361bc72c",
|
||||
"name.formatted": "Dell",
|
||||
"name.familyName": "Gay",
|
||||
"name.givenName": "Kyler",
|
||||
"name.middleName": "Hannah",
|
||||
"name.honorificPrefix": "Cassie",
|
||||
"name.honorificSuffix": "Yolanda",
|
||||
"displayName": "DPRLIJSFQMTL",
|
||||
"nickName": "BKSPMIRMFBTI",
|
||||
"title": "NBZCOAXVYJUY",
|
||||
"userType": "ZGJMYZRUORZE",
|
||||
"preferredLanguage": "as-IN",
|
||||
"locale": "JLOJHLPWZODG",
|
||||
"timezone": "America/Argentina/Rio_Gallegos",
|
||||
"active": True,
|
||||
f"{SCIM_URN_USER_ENTERPRISE}:employeeNumber": "PDFWRRZBQOHB",
|
||||
f"{SCIM_URN_USER_ENTERPRISE}:costCenter": "HACMZWSEDOTQ",
|
||||
f"{SCIM_URN_USER_ENTERPRISE}:organization": "LXVHJUOLNCLS",
|
||||
f"{SCIM_URN_USER_ENTERPRISE}:division": "JASVTPKPBPMG",
|
||||
f"{SCIM_URN_USER_ENTERPRISE}:department": "GMSBFLMNPABY",
|
||||
},
|
||||
},
|
||||
],
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"active": True,
|
||||
"addresses": [
|
||||
{
|
||||
"primary": "true",
|
||||
"formatted": "BLJMCNXHYLZK",
|
||||
"streetAddress": "7801 Jacobs Fork",
|
||||
"locality": "HZJBJWFAKXDD",
|
||||
"region": "GJXCXPMIIKWK",
|
||||
"postalCode": "pv82 8ua",
|
||||
"country": "India",
|
||||
}
|
||||
],
|
||||
"displayName": "KEFXCHKHAFOT",
|
||||
"emails": [{"primary": "true", "value": "scot@zemlak.uk"}],
|
||||
"entitlements": [{"primary": "true", "value": "FTTUXWYDAAQC"}],
|
||||
"externalId": "448d2786-7bf6-4e03-a4ef-64cbaf162fa7",
|
||||
"ims": [{"primary": "true", "value": "IGWZUUMCMKXS", "display": "PJVGMMKYYHRU"}],
|
||||
"locale": "PJNYJHWJILTI",
|
||||
"name": {
|
||||
"formatted": "Ladarius",
|
||||
"familyName": "Manley",
|
||||
"givenName": "Mazie",
|
||||
"middleName": "Vernon",
|
||||
"honorificPrefix": "Melyssa",
|
||||
"honorificSuffix": "Demarcus",
|
||||
},
|
||||
"nickName": "HTPKOXMWZKHL",
|
||||
"phoneNumbers": [
|
||||
{"primary": "true", "value": "50-608-7660", "display": "50-608-7660"}
|
||||
],
|
||||
"photos": [{"primary": "true", "display": "KCONLNLSYTBP"}],
|
||||
"preferredLanguage": "wae",
|
||||
"profileUrl": "HPSEOIPXMGOH",
|
||||
"roles": [{"primary": "true", "value": "TLGYITOIZGKP"}],
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"timezone": "America/Indiana/Petersburg",
|
||||
"title": "EJWFXLHNHMCD",
|
||||
SCIM_URN_USER_ENTERPRISE: {
|
||||
"employeeNumber": "XHDMEJUURJNR",
|
||||
"costCenter": "RXUYBXOTRCZH",
|
||||
"organization": "CEXWXMBRYAHN",
|
||||
"division": "XMPFMDCLRKCW",
|
||||
"department": "BKMNJVMCJUYS",
|
||||
"manager": "PNGSGXLYVWMV",
|
||||
},
|
||||
"userName": "imelda.auer@kshlerin.co.uk",
|
||||
"userType": "PZFXORVSUAPU",
|
||||
"x509Certificates": [{"primary": "true", "value": "KOVKWGIVVEHH"}],
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"active": True,
|
||||
"addresses": [
|
||||
{
|
||||
"primary": "true",
|
||||
"formatted": "BLJMCNXHYLZK",
|
||||
"streetAddress": "7801 Jacobs Fork",
|
||||
"locality": "HZJBJWFAKXDD",
|
||||
"region": "GJXCXPMIIKWK",
|
||||
"postalCode": "pv82 8ua",
|
||||
"country": "India",
|
||||
}
|
||||
],
|
||||
"displayName": "DPRLIJSFQMTL",
|
||||
"emails": [{"primary": "true", "value": "scot@zemlak.uk"}],
|
||||
"entitlements": [{"primary": "true", "value": "FTTUXWYDAAQC"}],
|
||||
"externalId": "7faaefb0-0774-4d8e-8f6d-863c361bc72c",
|
||||
"ims": [{"primary": "true", "value": "IGWZUUMCMKXS", "display": "PJVGMMKYYHRU"}],
|
||||
"locale": "JLOJHLPWZODG",
|
||||
"name": {
|
||||
"formatted": "Dell",
|
||||
"familyName": "Gay",
|
||||
"givenName": "Kyler",
|
||||
"middleName": "Hannah",
|
||||
"honorificPrefix": "Cassie",
|
||||
"honorificSuffix": "Yolanda",
|
||||
},
|
||||
"nickName": "BKSPMIRMFBTI",
|
||||
"phoneNumbers": [
|
||||
{"primary": "true", "value": "50-608-7660", "display": "50-608-7660"}
|
||||
],
|
||||
"photos": [{"primary": "true", "display": "KCONLNLSYTBP"}],
|
||||
"preferredLanguage": "as-IN",
|
||||
"profileUrl": "HPSEOIPXMGOH",
|
||||
"roles": [{"primary": "true", "value": "TLGYITOIZGKP"}],
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"timezone": "America/Argentina/Rio_Gallegos",
|
||||
"title": "NBZCOAXVYJUY",
|
||||
SCIM_URN_USER_ENTERPRISE: {
|
||||
"employeeNumber": "PDFWRRZBQOHB",
|
||||
"costCenter": "HACMZWSEDOTQ",
|
||||
"organization": "LXVHJUOLNCLS",
|
||||
"division": "JASVTPKPBPMG",
|
||||
"department": "GMSBFLMNPABY",
|
||||
"manager": "PNGSGXLYVWMV",
|
||||
},
|
||||
"userName": "imelda.auer@kshlerin.co.uk",
|
||||
"userType": "ZGJMYZRUORZE",
|
||||
"x509Certificates": [{"primary": "true", "value": "KOVKWGIVVEHH"}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_schema_urn_manager(self):
|
||||
req = {
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "Add",
|
||||
"value": {
|
||||
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager": "foo"
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
user = create_test_user()
|
||||
source = SCIMSource.objects.create(slug=generate_id())
|
||||
connection = SCIMSourceUser.objects.create(
|
||||
user=user,
|
||||
id=generate_id(),
|
||||
source=source,
|
||||
attributes={
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
},
|
||||
)
|
||||
updated = SCIMPatchProcessor().apply_patches(connection.attributes, req["Operations"])
|
||||
self.assertEqual(
|
||||
updated,
|
||||
{
|
||||
"meta": {"resourceType": "User"},
|
||||
"active": True,
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
SCIM_URN_USER_ENTERPRISE,
|
||||
],
|
||||
"userName": "test@t.goauthentik.io",
|
||||
"externalId": "test",
|
||||
"displayName": "Test MS",
|
||||
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
|
||||
"manager": {"value": "foo"}
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
"""SCIM Utils"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Page, Paginator
|
||||
@@ -21,6 +22,7 @@ from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
from authentik.sources.scim.views.v2.auth import SCIMTokenAuth
|
||||
from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError
|
||||
|
||||
SCIM_CONTENT_TYPE = "application/scim+json"
|
||||
|
||||
@@ -54,6 +56,13 @@ class SCIMView(APIView):
|
||||
def get_authenticators(self):
|
||||
return [SCIMTokenAuth(self)]
|
||||
|
||||
def remove_excluded_attributes(self, data: dict):
|
||||
"""Remove attributes specified in excludedAttributes"""
|
||||
excluded: str = self.request.query_params.get("excludedAttributes", "")
|
||||
for key in excluded.split(","):
|
||||
data.pop(key.strip(), None)
|
||||
return data
|
||||
|
||||
def filter_parse(self, request: Request):
|
||||
"""Parse the path of a Patch Operation"""
|
||||
path = request.query_params.get("filter")
|
||||
@@ -103,6 +112,12 @@ class SCIMObjectView(SCIMView):
|
||||
# a source attribute before
|
||||
self.mapper = SourceMapper(self.source)
|
||||
self.manager = self.mapper.get_manager(self.model, ["data"])
|
||||
for key, value in kwargs.items():
|
||||
if key.endswith("_id"):
|
||||
try:
|
||||
UUID(value)
|
||||
except ValueError:
|
||||
raise SCIMNotFoundError("Invalid ID") from None
|
||||
|
||||
def build_object_properties(self, data: dict[str, Any]) -> dict[str, Any | dict[str, Any]]:
|
||||
return self.mapper.build_object_properties(
|
||||
|
||||
@@ -17,6 +17,7 @@ from authentik.core.models import Group, User
|
||||
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOp, PatchOperation
|
||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
|
||||
from authentik.sources.scim.models import SCIMSourceGroup
|
||||
from authentik.sources.scim.patch.processor import SCIMPatchProcessor
|
||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||
from authentik.sources.scim.views.v2.exceptions import (
|
||||
SCIMConflictError,
|
||||
@@ -35,11 +36,12 @@ class GroupsView(SCIMObjectView):
|
||||
payload = SCIMGroupModel(
|
||||
schemas=[SCIM_GROUP_SCHEMA],
|
||||
id=str(scim_group.group.pk),
|
||||
externalId=scim_group.id,
|
||||
externalId=scim_group.external_id,
|
||||
displayName=scim_group.group.name,
|
||||
members=[],
|
||||
meta={
|
||||
"resourceType": "Group",
|
||||
"lastModified": scim_group.last_update,
|
||||
"location": self.request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-groups",
|
||||
@@ -54,7 +56,11 @@ class GroupsView(SCIMObjectView):
|
||||
for member in scim_group.group.users.order_by("pk"):
|
||||
member: User
|
||||
payload.members.append(GroupMember(value=str(member.uuid)))
|
||||
return payload.model_dump(mode="json", exclude_unset=True)
|
||||
final_payload = payload.model_dump(mode="json", exclude_unset=True)
|
||||
final_payload.update(scim_group.attributes)
|
||||
return self.remove_excluded_attributes(
|
||||
SCIMGroupModel.model_validate(final_payload).model_dump(mode="json", exclude_unset=True)
|
||||
)
|
||||
|
||||
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
|
||||
"""List Group handler"""
|
||||
@@ -81,7 +87,7 @@ class GroupsView(SCIMObjectView):
|
||||
)
|
||||
|
||||
@atomic
|
||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict, apply_members=True):
|
||||
"""Partial update a group"""
|
||||
properties = self.build_object_properties(data)
|
||||
|
||||
@@ -94,7 +100,7 @@ class GroupsView(SCIMObjectView):
|
||||
|
||||
group.update_attributes(properties)
|
||||
|
||||
if "members" in data:
|
||||
if "members" in data and apply_members:
|
||||
query = Q()
|
||||
for _member in data.get("members", []):
|
||||
try:
|
||||
@@ -105,14 +111,18 @@ class GroupsView(SCIMObjectView):
|
||||
query |= Q(uuid=member.value)
|
||||
if query:
|
||||
group.users.set(User.objects.filter(query))
|
||||
data["members"] = self._convert_members(group)
|
||||
if not connection:
|
||||
connection, _ = SCIMSourceGroup.objects.get_or_create(
|
||||
connection, _ = SCIMSourceGroup.objects.update_or_create(
|
||||
external_id=data.get("externalId") or str(uuid4()),
|
||||
source=self.source,
|
||||
group=group,
|
||||
attributes=data,
|
||||
id=data.get("externalId") or str(uuid4()),
|
||||
defaults={
|
||||
"attributes": data,
|
||||
},
|
||||
)
|
||||
else:
|
||||
connection.external_id = data.get("externalId", connection.external_id)
|
||||
connection.attributes = data
|
||||
connection.save()
|
||||
return connection
|
||||
@@ -139,6 +149,12 @@ class GroupsView(SCIMObjectView):
|
||||
connection = self.update_group(connection, request.data)
|
||||
return Response(self.group_to_scim(connection), status=200)
|
||||
|
||||
def _convert_members(self, group: Group):
|
||||
users = []
|
||||
for user in group.users.all().order_by("uuid"):
|
||||
users.append({"value": str(user.uuid)})
|
||||
return sorted(users, key=lambda u: u["value"])
|
||||
|
||||
@atomic
|
||||
def patch(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||
"""Patch group handler"""
|
||||
@@ -171,6 +187,13 @@ class GroupsView(SCIMObjectView):
|
||||
query |= Q(uuid=member["value"])
|
||||
if query:
|
||||
connection.group.users.remove(*User.objects.filter(query))
|
||||
patcher = SCIMPatchProcessor()
|
||||
patched_data = patcher.apply_patches(
|
||||
connection.attributes, request.data.get("Operations", [])
|
||||
)
|
||||
patched_data["members"] = self._convert_members(connection.group)
|
||||
if patched_data != connection.attributes:
|
||||
self.update_group(connection, patched_data, apply_members=False)
|
||||
return Response(self.group_to_scim(connection), status=200)
|
||||
|
||||
@atomic
|
||||
|
||||
@@ -33,9 +33,7 @@ class ServiceProviderConfigView(SCIMView):
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
||||
"authenticationSchemes": auth_schemas,
|
||||
# We only support patch for groups currently, so don't broadly advertise it.
|
||||
# Implementations that require Group patch will use it regardless of this flag.
|
||||
"patch": {"supported": False},
|
||||
"patch": {"supported": True},
|
||||
"bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0},
|
||||
"filter": {
|
||||
"supported": True,
|
||||
|
||||
@@ -15,6 +15,7 @@ from authentik.core.models import User
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserModel
|
||||
from authentik.sources.scim.models import SCIMSourceUser
|
||||
from authentik.sources.scim.patch.processor import SCIMPatchProcessor
|
||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||
from authentik.sources.scim.views.v2.exceptions import SCIMConflictError, SCIMNotFoundError
|
||||
|
||||
@@ -29,7 +30,7 @@ class UsersView(SCIMObjectView):
|
||||
payload = SCIMUserModel(
|
||||
schemas=[SCIM_USER_SCHEMA],
|
||||
id=str(scim_user.user.uuid),
|
||||
externalId=scim_user.id,
|
||||
externalId=scim_user.external_id,
|
||||
userName=scim_user.user.username,
|
||||
name=Name(
|
||||
formatted=scim_user.user.name,
|
||||
@@ -44,8 +45,7 @@ class UsersView(SCIMObjectView):
|
||||
meta={
|
||||
"resourceType": "User",
|
||||
"created": scim_user.user.date_joined,
|
||||
# TODO: use events to find last edit?
|
||||
"lastModified": scim_user.user.date_joined,
|
||||
"lastModified": scim_user.last_update,
|
||||
"location": self.request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
@@ -59,7 +59,9 @@ class UsersView(SCIMObjectView):
|
||||
)
|
||||
final_payload = payload.model_dump(mode="json", exclude_unset=True)
|
||||
final_payload.update(scim_user.attributes)
|
||||
return final_payload
|
||||
return self.remove_excluded_attributes(
|
||||
SCIMUserModel.model_validate(final_payload).model_dump(mode="json", exclude_unset=True)
|
||||
)
|
||||
|
||||
def get(self, request: Request, user_id: str | None = None, **kwargs) -> Response:
|
||||
"""List User handler"""
|
||||
@@ -101,13 +103,16 @@ class UsersView(SCIMObjectView):
|
||||
user.update_attributes(properties)
|
||||
|
||||
if not connection:
|
||||
connection, _ = SCIMSourceUser.objects.get_or_create(
|
||||
connection, _ = SCIMSourceUser.objects.update_or_create(
|
||||
external_id=data.get("externalId") or str(uuid4()),
|
||||
source=self.source,
|
||||
user=user,
|
||||
attributes=data,
|
||||
id=data.get("externalId") or str(uuid4()),
|
||||
defaults={
|
||||
"attributes": data,
|
||||
},
|
||||
)
|
||||
else:
|
||||
connection.external_id = data.get("externalId", connection.external_id)
|
||||
connection.attributes = data
|
||||
connection.save()
|
||||
return connection
|
||||
@@ -127,6 +132,18 @@ class UsersView(SCIMObjectView):
|
||||
connection = self.update_user(None, request.data)
|
||||
return Response(self.user_to_scim(connection), status=201)
|
||||
|
||||
def patch(self, request: Request, user_id: str, **kwargs):
|
||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||
if not connection:
|
||||
raise SCIMNotFoundError("User not found.")
|
||||
patcher = SCIMPatchProcessor()
|
||||
patched_data = patcher.apply_patches(
|
||||
connection.attributes, request.data.get("Operations", [])
|
||||
)
|
||||
if patched_data != connection.attributes:
|
||||
self.update_user(connection, patched_data)
|
||||
return Response(self.user_to_scim(connection), status=200)
|
||||
|
||||
def put(self, request: Request, user_id: str, **kwargs) -> Response:
|
||||
"""Update user handler"""
|
||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||
|
||||
@@ -13,7 +13,6 @@ from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.stages.authenticator.models import SideChannelDevice
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@@ -160,9 +159,8 @@ class EmailDevice(SerializerModel, SideChannelDevice):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=stage.template,
|
||||
).from_http(self.request)
|
||||
).with_exception(exc).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -17,7 +17,6 @@ from authentik.flows.challenge import (
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.email import mask_email
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.authenticator_email.models import (
|
||||
AuthenticatorEmailStage,
|
||||
@@ -100,9 +99,8 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=stage.template,
|
||||
).from_http(self.request)
|
||||
).with_exception(exc).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def _has_email(self) -> str | None:
|
||||
|
||||
@@ -19,7 +19,6 @@ from authentik.events.models import Event, EventAction, NotificationWebhookMappi
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.stages.authenticator.models import SideChannelDevice
|
||||
|
||||
@@ -142,10 +141,9 @@ class AuthenticatorSMSStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="Error sending SMS",
|
||||
exc=exception_to_string(exc),
|
||||
status_code=response.status_code,
|
||||
body=response.text,
|
||||
).set_user(device.user).save()
|
||||
).with_exception(exc).set_user(device.user).save()
|
||||
if response.status_code >= HttpResponseBadRequest.status_code:
|
||||
raise ValidationError(response.text) from None
|
||||
raise
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,7 +21,6 @@ from authentik.flows.models import FlowDesignation, FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.email.flow import pickle_flow_token_for_email
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@@ -129,9 +128,8 @@ class EmailStageView(ChallengeStageView):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=current_stage.template,
|
||||
).from_http(self.request)
|
||||
).with_exception(exc).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
@@ -145,7 +143,7 @@ class EmailStageView(ChallengeStageView):
|
||||
messages.success(request, _("Successfully verified Email."))
|
||||
if self.executor.current_stage.activate_user_on_success:
|
||||
user.is_active = True
|
||||
user.save()
|
||||
user.save(update_fields=["is_active"])
|
||||
return self.executor.stage_ok()
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
self.logger.debug("No pending user")
|
||||
|
||||
@@ -191,9 +191,10 @@ class ListPolicyEngine(PolicyEngine):
|
||||
self.use_cache = False
|
||||
|
||||
def bindings(self):
|
||||
for policy in self.__list:
|
||||
for idx, policy in enumerate(self.__list):
|
||||
yield PolicyBinding(
|
||||
policy=policy,
|
||||
order=idx,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class TestPromptStage(FlowTestCase):
|
||||
"""Test challenge_response validation"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
expr = "False"
|
||||
expr_policy = ExpressionPolicy.objects.create(name="validate-form", expression=expr)
|
||||
expr_policy = ExpressionPolicy.objects.create(name=generate_id(), expression=expr)
|
||||
self.stage.validation_policies.set([expr_policy])
|
||||
self.stage.save()
|
||||
challenge_response = PromptChallengeResponse(
|
||||
@@ -222,6 +222,18 @@ class TestPromptStage(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(challenge_response.is_valid(), False)
|
||||
|
||||
def test_invalid_challenge_multiple(self):
|
||||
"""Test challenge_response validation (multiple policies)"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
expr_policy1 = ExpressionPolicy.objects.create(name=generate_id(), expression="False")
|
||||
expr_policy2 = ExpressionPolicy.objects.create(name=generate_id(), expression="False")
|
||||
self.stage.validation_policies.set([expr_policy1, expr_policy2])
|
||||
self.stage.save()
|
||||
challenge_response = PromptChallengeResponse(
|
||||
None, stage_instance=self.stage, plan=plan, data=self.prompt_data, stage=self.stage_view
|
||||
)
|
||||
self.assertEqual(challenge_response.is_valid(), False)
|
||||
|
||||
def test_valid_challenge_request(self):
|
||||
"""Test a request with valid challenge_response data"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
@@ -234,7 +246,7 @@ class TestPromptStage(FlowTestCase):
|
||||
"return request.context['prompt_data']['password_prompt'] "
|
||||
"== request.context['prompt_data']['password2_prompt']"
|
||||
)
|
||||
expr_policy = ExpressionPolicy.objects.create(name="validate-form", expression=expr)
|
||||
expr_policy = ExpressionPolicy.objects.create(name=generate_id(), expression=expr)
|
||||
self.stage.validation_policies.set([expr_policy])
|
||||
self.stage.save()
|
||||
challenge_response = PromptChallengeResponse(
|
||||
|
||||
@@ -10961,6 +10961,7 @@
|
||||
"enum": [
|
||||
"apple",
|
||||
"openidconnect",
|
||||
"entraid",
|
||||
"azuread",
|
||||
"discord",
|
||||
"facebook",
|
||||
|
||||
8
go.mod
8
go.mod
@@ -10,14 +10,14 @@ require (
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/grafana/pyroscope-go v1.2.2
|
||||
github.com/grafana/pyroscope-go v1.2.3
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
@@ -29,10 +29,10 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025063.3
|
||||
goauthentik.io/api/v3 v3.2025063.5
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sync v0.16.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -115,8 +115,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -180,8 +180,8 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
|
||||
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
||||
github.com/grafana/pyroscope-go v1.2.3 h1:Rp8mjqqGqmRDvV6XYmuedUAv7wVnQJK/M1pBt6uNwxU=
|
||||
github.com/grafana/pyroscope-go v1.2.3/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
@@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025063.3 h1:Ci+iKWgyioG6QYN3yTZn0SLEnGLC8uLu4FUqMdF5AP8=
|
||||
goauthentik.io/api/v3 v3.2025063.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025063.5 h1:j5el9/qI/72Q5x5QAiMzgQTswMj2TK3h74OaBcFEtkI=
|
||||
goauthentik.io/api/v3 v3.2025063.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -384,8 +384,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
@@ -100,6 +100,9 @@ elif [[ "$1" == "healthcheck" ]]; then
|
||||
elif [[ "$1" == "dump_config" ]]; then
|
||||
shift
|
||||
exec python -m authentik.lib.config $@
|
||||
elif [[ "$1" == "support" ]]; then
|
||||
wait_for_db
|
||||
exec python -m lifecycle.support
|
||||
elif [[ "$1" == "debug" ]]; then
|
||||
exec sleep infinity
|
||||
else
|
||||
|
||||
9
lifecycle/aws/package-lock.json
generated
9
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1020.2",
|
||||
"aws-cdk": "^2.1021.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -17,10 +17,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1020.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1020.2.tgz",
|
||||
"integrity": "sha512-yWdt3dJh4aPm1VNyEgfG3lozGrvddw0i7avt+Cl9KOYixmisQtAg39/aZqzVVqjzVZVEanXmz+tlhzzh75Z69A==",
|
||||
"version": "2.1021.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1021.0.tgz",
|
||||
"integrity": "sha512-kE557b4N9UFWax+7km3R6D56o4tGhpzOks/lRDugaoC8su3mocLCXJhb954b/IRl0ipnbZnY/Sftq+RQ/sxivg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"cdk": "bin/cdk"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1020.2",
|
||||
"aws-cdk": "^2.1021.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ from os import environ, system
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from psycopg import Connection, Cursor, connect
|
||||
from psycopg import Connection, Cursor
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.config import CONFIG, django_db_config
|
||||
from lifecycle.wait_for_db import get_postgres
|
||||
|
||||
LOGGER = get_logger()
|
||||
ADV_LOCK_UID = 1000
|
||||
@@ -71,17 +72,7 @@ def release_lock(cursor: Cursor):
|
||||
|
||||
|
||||
def run_migrations():
|
||||
conn = connect(
|
||||
dbname=CONFIG.get("postgresql.name"),
|
||||
user=CONFIG.get("postgresql.user"),
|
||||
password=CONFIG.get("postgresql.password"),
|
||||
host=CONFIG.get("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
conn = get_postgres()
|
||||
curr = conn.cursor()
|
||||
try:
|
||||
wait_for_lock(curr)
|
||||
|
||||
111
lifecycle/support.py
Normal file
111
lifecycle/support.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import platform
|
||||
from hashlib import sha512
|
||||
from pprint import pprint
|
||||
from ssl import OPENSSL_VERSION
|
||||
from sys import version as python_version
|
||||
|
||||
from cryptography.exceptions import InternalError
|
||||
from cryptography.hazmat.backends.openssl.backend import backend
|
||||
from jwcrypto.common import json_encode
|
||||
from jwcrypto.jwe import JWE
|
||||
from jwcrypto.jwk import JWK
|
||||
from jwt import encode
|
||||
from psutil import cpu_count, virtual_memory
|
||||
from redis import Redis
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
from authentik.root.install_id import get_install_id_raw
|
||||
from lifecycle.wait_for_db import get_postgres, get_redis
|
||||
|
||||
try:
|
||||
backend._enable_fips()
|
||||
except InternalError:
|
||||
pass
|
||||
|
||||
|
||||
def get_version_history():
|
||||
with get_postgres() as postgres:
|
||||
cur = postgres.cursor()
|
||||
cur.execute("""SELECT "timestamp", "version", "build" FROM authentik_version_history;""")
|
||||
for x, y, z in cur.fetchall():
|
||||
yield (x.timestamp(), y, z)
|
||||
|
||||
|
||||
def get_postgres_version():
|
||||
with get_postgres() as postgres:
|
||||
cur = postgres.cursor()
|
||||
cur.execute("""SELECT version();""")
|
||||
return cur.fetchone()[0]
|
||||
|
||||
|
||||
def get_redis_version():
|
||||
redis: Redis = get_redis()
|
||||
version = redis.info()
|
||||
redis.close()
|
||||
return f"{version["redis_version"]} {version["redis_mode"]} {version["os"]}"
|
||||
|
||||
|
||||
def get_limited_config():
|
||||
return {
|
||||
"postgresql": {
|
||||
"host": CONFIG.get("postgresql.host"),
|
||||
},
|
||||
"redis": {
|
||||
"host": CONFIG.get("redis.host"),
|
||||
},
|
||||
"debug": CONFIG.get_bool("debug"),
|
||||
"log_level": CONFIG.get("log_level"),
|
||||
"error_reporting": {
|
||||
"enabled": CONFIG.get_bool("error_reporting.enabled"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def generate():
|
||||
payload = {
|
||||
"version": {
|
||||
"history": list(get_version_history()),
|
||||
"current": get_full_version(),
|
||||
"postgres": get_postgres_version(),
|
||||
"redis": get_redis_version(),
|
||||
"ssl": OPENSSL_VERSION,
|
||||
"python": python_version,
|
||||
},
|
||||
"env": get_env(),
|
||||
"install_id_hash": sha512(get_install_id_raw().encode("ascii")).hexdigest()[:16],
|
||||
"system": {
|
||||
"cpu": {"count": cpu_count()},
|
||||
"fips": backend._fips_enabled,
|
||||
"memory_bytes": virtual_memory().total,
|
||||
"architecture": platform.machine(),
|
||||
"platform": platform.platform(),
|
||||
"uname": " ".join(platform.uname()),
|
||||
},
|
||||
"config": get_limited_config(),
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def encrypt(raw):
|
||||
with open("authentik/enterprise/public.pem", "rb") as _key:
|
||||
key = JWK.from_pem(_key.read())
|
||||
jwe = JWE(
|
||||
encode(raw, "foo"),
|
||||
json_encode(
|
||||
{
|
||||
"alg": "ECDH-ES+A256KW",
|
||||
"enc": "A256CBC-HS512",
|
||||
"typ": "JWE",
|
||||
}
|
||||
),
|
||||
)
|
||||
jwe.add_recipient(key)
|
||||
return jwe.serialize(compact=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = generate()
|
||||
snippet = encrypt(data)
|
||||
pprint(data)
|
||||
@@ -13,23 +13,32 @@ from authentik.lib.config import CONFIG, redis_url
|
||||
CHECK_THRESHOLD = 30
|
||||
|
||||
|
||||
def get_postgres():
|
||||
return connect(
|
||||
dbname=CONFIG.get("postgresql.name"),
|
||||
user=CONFIG.get("postgresql.user"),
|
||||
password=CONFIG.get("postgresql.password"),
|
||||
host=CONFIG.get("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
|
||||
|
||||
def get_redis():
|
||||
url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db"))
|
||||
return Redis.from_url(url)
|
||||
|
||||
|
||||
def check_postgres():
|
||||
attempt = 0
|
||||
while True:
|
||||
if attempt >= CHECK_THRESHOLD:
|
||||
sysexit(1)
|
||||
try:
|
||||
conn = connect(
|
||||
dbname=CONFIG.refresh("postgresql.name"),
|
||||
user=CONFIG.refresh("postgresql.user"),
|
||||
password=CONFIG.refresh("postgresql.password"),
|
||||
host=CONFIG.refresh("postgresql.host"),
|
||||
port=CONFIG.get_int("postgresql.port"),
|
||||
sslmode=CONFIG.get("postgresql.sslmode"),
|
||||
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
|
||||
sslcert=CONFIG.get("postgresql.sslcert"),
|
||||
sslkey=CONFIG.get("postgresql.sslkey"),
|
||||
)
|
||||
conn = get_postgres()
|
||||
conn.cursor()
|
||||
break
|
||||
except OperationalError as exc:
|
||||
@@ -41,13 +50,12 @@ def check_postgres():
|
||||
|
||||
|
||||
def check_redis():
|
||||
url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db"))
|
||||
attempt = 0
|
||||
while True:
|
||||
if attempt >= CHECK_THRESHOLD:
|
||||
sysexit(1)
|
||||
try:
|
||||
redis = Redis.from_url(url)
|
||||
redis = get_redis()
|
||||
redis.ping()
|
||||
break
|
||||
except RedisError as exc:
|
||||
|
||||
@@ -40,7 +40,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-05 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-07-15 00:11+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Fabian, 2025\n"
|
||||
"Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n"
|
||||
@@ -407,7 +407,7 @@ msgstr "Eigenschaft"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Property Mappings"
|
||||
msgstr "Eigenschaften"
|
||||
msgstr "Property Mappings"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "session data"
|
||||
@@ -593,7 +593,7 @@ msgstr "Google Workspace Provider Gruppen"
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Property mappings used for group creation/updating."
|
||||
msgstr ""
|
||||
"Eigenschaft, die für die Erstellung/Aktualisierung von Gruppen verwendet "
|
||||
"Eigenschaften, die für die Erstellung/Aktualisierung von Gruppen verwendet "
|
||||
"werden."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
@@ -2754,6 +2754,14 @@ msgstr "Azure AD OAuth Quelle"
|
||||
msgid "Azure AD OAuth Sources"
|
||||
msgstr "Azure AD OAuth Quellen"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Source"
|
||||
msgstr "Entra ID OAuth-Quelle"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Sources"
|
||||
msgstr "Entra ID OAuth-Quellen"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OpenID OAuth Source"
|
||||
msgstr "OpenID OAuth Quelle"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-05 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-07-15 00:11+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"
|
||||
@@ -2454,6 +2454,14 @@ msgstr ""
|
||||
msgid "Azure AD OAuth Sources"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Source"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Sources"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OpenID OAuth Source"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-07-05 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Iamanaws, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n"
|
||||
@@ -125,10 +125,6 @@ msgstr "Marcas"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "El usuario no tiene acceso a la aplicación"
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "Descripción adicional no disponible."
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "No se puede establecer un grupo como su propio padre."
|
||||
@@ -375,6 +371,10 @@ msgstr "Tokens"
|
||||
msgid "View token's key"
|
||||
msgstr "Ver llave del token"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Set a token's key"
|
||||
msgstr "Establecer la clave de un token"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Property Mapping"
|
||||
msgstr "Asignación de Propiedades"
|
||||
@@ -838,6 +838,15 @@ msgstr ""
|
||||
"Define a qué grupo de usuarios se les debe enviar y mostrar esta "
|
||||
"notificación. Si se deja vacío, no se enviará Notificación."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr ""
|
||||
"Cuando está habilitada, se enviará una notificación al usuario que "
|
||||
"desencadenó el evento. Cuando se configura destination_group, la "
|
||||
"notificación se enviará a ambos."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "Regla de notificación"
|
||||
@@ -3869,6 +3878,17 @@ msgstr ""
|
||||
"Recordarme. El valor predeterminado de 0 significa que no se mostrará la "
|
||||
"opción Recordarme. (Formato: hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"When set to a non-zero value, authentik will save a cookie with a longer "
|
||||
"expiry,to remember the device the user is logging in from. (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
"Cuando se establece en un valor distinto de cero, authentik guardará una "
|
||||
"cookie con una expiración más larga para recordar el dispositivo desde el "
|
||||
"cual el usuario está iniciando sesión. (Formato: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid "User Login Stage"
|
||||
msgstr "Etapa de inicio de sesión"
|
||||
|
||||
Binary file not shown.
@@ -20,7 +20,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-07-05 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
@@ -373,6 +373,10 @@ msgstr "Tokens"
|
||||
msgid "View token's key"
|
||||
msgstr "Visualizza la chiave token"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Set a token's key"
|
||||
msgstr "Imposta la chiave di un token"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Property Mapping"
|
||||
msgstr "Mappatura della proprietà"
|
||||
@@ -3868,6 +3872,16 @@ msgstr ""
|
||||
"ricordami. Il valore predefinito di 0 significa che l'opzione ricordami non "
|
||||
"verrà mostrata. (Formato: hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"When set to a non-zero value, authentik will save a cookie with a longer "
|
||||
"expiry,to remember the device the user is logging in from. (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
"Se impostato su un valore diverso da zero, authentik salverà un cookie con "
|
||||
"una scadenza più lunga, per ricordare il dispositivo da cui l'utente sta "
|
||||
"effettuando l'accesso. (Formato: ore=-1; minuti=-2; secondi=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid "User Login Stage"
|
||||
msgstr "Fase di accesso utente"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
18
packages/docusaurus-config/package-lock.json
generated
18
packages/docusaurus-config/package-lock.json
generated
@@ -4452,9 +4452,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
|
||||
"integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4462,9 +4462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz",
|
||||
"integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==",
|
||||
"version": "19.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -14661,9 +14661,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
250
packages/esbuild-plugin-live-reload/package-lock.json
generated
250
packages/esbuild-plugin-live-reload/package-lock.json
generated
@@ -132,9 +132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
||||
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -149,9 +149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
|
||||
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -166,9 +166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -183,9 +183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -200,9 +200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -217,9 +217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -234,9 +234,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -251,9 +251,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -268,9 +268,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
|
||||
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -285,9 +285,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -302,9 +302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
|
||||
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -319,9 +319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
|
||||
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -336,9 +336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
|
||||
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -353,9 +353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
|
||||
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -370,9 +370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
|
||||
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -387,9 +387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
|
||||
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -404,9 +404,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -421,9 +421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -438,9 +438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -455,9 +455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -472,9 +472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -488,10 +488,27 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -506,9 +523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -523,9 +540,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
|
||||
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -540,9 +557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -887,9 +904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"version": "24.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
|
||||
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1252,9 +1269,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1265,31 +1282,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.5",
|
||||
"@esbuild/android-arm": "0.25.5",
|
||||
"@esbuild/android-arm64": "0.25.5",
|
||||
"@esbuild/android-x64": "0.25.5",
|
||||
"@esbuild/darwin-arm64": "0.25.5",
|
||||
"@esbuild/darwin-x64": "0.25.5",
|
||||
"@esbuild/freebsd-arm64": "0.25.5",
|
||||
"@esbuild/freebsd-x64": "0.25.5",
|
||||
"@esbuild/linux-arm": "0.25.5",
|
||||
"@esbuild/linux-arm64": "0.25.5",
|
||||
"@esbuild/linux-ia32": "0.25.5",
|
||||
"@esbuild/linux-loong64": "0.25.5",
|
||||
"@esbuild/linux-mips64el": "0.25.5",
|
||||
"@esbuild/linux-ppc64": "0.25.5",
|
||||
"@esbuild/linux-riscv64": "0.25.5",
|
||||
"@esbuild/linux-s390x": "0.25.5",
|
||||
"@esbuild/linux-x64": "0.25.5",
|
||||
"@esbuild/netbsd-arm64": "0.25.5",
|
||||
"@esbuild/netbsd-x64": "0.25.5",
|
||||
"@esbuild/openbsd-arm64": "0.25.5",
|
||||
"@esbuild/openbsd-x64": "0.25.5",
|
||||
"@esbuild/sunos-x64": "0.25.5",
|
||||
"@esbuild/win32-arm64": "0.25.5",
|
||||
"@esbuild/win32-ia32": "0.25.5",
|
||||
"@esbuild/win32-x64": "0.25.5"
|
||||
"@esbuild/aix-ppc64": "0.25.6",
|
||||
"@esbuild/android-arm": "0.25.6",
|
||||
"@esbuild/android-arm64": "0.25.6",
|
||||
"@esbuild/android-x64": "0.25.6",
|
||||
"@esbuild/darwin-arm64": "0.25.6",
|
||||
"@esbuild/darwin-x64": "0.25.6",
|
||||
"@esbuild/freebsd-arm64": "0.25.6",
|
||||
"@esbuild/freebsd-x64": "0.25.6",
|
||||
"@esbuild/linux-arm": "0.25.6",
|
||||
"@esbuild/linux-arm64": "0.25.6",
|
||||
"@esbuild/linux-ia32": "0.25.6",
|
||||
"@esbuild/linux-loong64": "0.25.6",
|
||||
"@esbuild/linux-mips64el": "0.25.6",
|
||||
"@esbuild/linux-ppc64": "0.25.6",
|
||||
"@esbuild/linux-riscv64": "0.25.6",
|
||||
"@esbuild/linux-s390x": "0.25.6",
|
||||
"@esbuild/linux-x64": "0.25.6",
|
||||
"@esbuild/netbsd-arm64": "0.25.6",
|
||||
"@esbuild/netbsd-x64": "0.25.6",
|
||||
"@esbuild/openbsd-arm64": "0.25.6",
|
||||
"@esbuild/openbsd-x64": "0.25.6",
|
||||
"@esbuild/openharmony-arm64": "0.25.6",
|
||||
"@esbuild/sunos-x64": "0.25.6",
|
||||
"@esbuild/win32-arm64": "0.25.6",
|
||||
"@esbuild/win32-ia32": "0.25.6",
|
||||
"@esbuild/win32-x64": "0.25.6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
@@ -2242,9 +2260,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2310,13 +2328,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-packagejson": {
|
||||
"version": "2.5.17",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.17.tgz",
|
||||
"integrity": "sha512-1WYvhTix+4EMYZQYSjAxb6+KTCULINuHUTBcxYa2ipoUS9Y2zJVjE3kuZ5I7ZWIFqyK8xpwYIunXqN5eiT7Hew==",
|
||||
"version": "2.5.18",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.18.tgz",
|
||||
"integrity": "sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sort-package-json": "3.3.1",
|
||||
"sort-package-json": "3.4.0",
|
||||
"synckit": "0.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2489,9 +2507,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.3.1.tgz",
|
||||
"integrity": "sha512-awjhQR2Iy5UN3NuguAK5+RezcEuUg9Ra4O8y2Aj+DlJa7MywyHaipAPf9bu4qqFj0hsYHHoT9sS3aV7Ucu728g==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.4.0.tgz",
|
||||
"integrity": "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
193
packages/eslint-config/package-lock.json
generated
193
packages/eslint-config/package-lock.json
generated
@@ -272,9 +272,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
|
||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -453,6 +454,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
@@ -466,6 +468,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
@@ -475,6 +478,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
@@ -569,16 +573,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
||||
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
|
||||
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/type-utils": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/type-utils": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -592,7 +597,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.36.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
@@ -602,20 +607,22 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
||||
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
||||
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -631,13 +638,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
|
||||
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||
"@typescript-eslint/types": "^8.36.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.37.0",
|
||||
"@typescript-eslint/types": "^8.37.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -652,13 +660,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
|
||||
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0"
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -669,10 +678,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -685,13 +695,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -708,10 +720,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
|
||||
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -721,15 +734,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
|
||||
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.36.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||
"@typescript-eslint/project-service": "8.37.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -753,6 +767,7 @@
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -762,6 +777,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -777,6 +793,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -785,15 +802,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
|
||||
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.36.0",
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/typescript-estree": "8.36.0"
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -808,12 +826,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
|
||||
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1065,9 +1084,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -1079,6 +1098,7 @@
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@@ -1537,17 +1557,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
|
||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.3.0",
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/core": "^0.15.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@eslint/js": "9.31.0",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -1801,6 +1822,18 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/core": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
@@ -1871,6 +1904,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
@@ -1887,6 +1921,7 @@
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
@@ -1911,6 +1946,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
@@ -1932,6 +1968,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -2148,7 +2185,8 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
@@ -2495,6 +2533,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -2877,6 +2916,7 @@
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
@@ -2886,6 +2926,7 @@
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
@@ -3170,6 +3211,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -3288,7 +3330,8 @@
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
@@ -3397,6 +3440,7 @@
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
@@ -3421,6 +3465,7 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
@@ -3896,6 +3941,7 @@
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -3908,6 +3954,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
@@ -4028,14 +4075,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
||||
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz",
|
||||
"integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.36.0",
|
||||
"@typescript-eslint/parser": "8.36.0",
|
||||
"@typescript-eslint/utils": "8.36.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.37.0",
|
||||
"@typescript-eslint/parser": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -124,12 +124,13 @@ const useLegacyCleanup = process.env.AK_FIX_LEGACY_IMPORTS === "true";
|
||||
* @param {ParserOptions} options
|
||||
*/
|
||||
const preprocess = (input, { filepath, printWidth }) => {
|
||||
if (input?.includes("ts-import-sorter: disable")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
let output = input;
|
||||
|
||||
console.log("Looking for JSDoc...");
|
||||
if (output.startsWith("/**\n")) {
|
||||
console.log("JSDoc detected. Adding double newline if not present");
|
||||
|
||||
output = output.replace(/(^\s\*\/\n)(import)/m, "$1\n$2");
|
||||
}
|
||||
|
||||
@@ -144,6 +145,7 @@ const preprocess = (input, { filepath, printWidth }) => {
|
||||
wrappingStyle: "prettier",
|
||||
groupRules: [
|
||||
"^node:",
|
||||
"^[./]",
|
||||
...webSubmodules.map((submodule) => `^(@goauthentik/|#)${submodule}.+`),
|
||||
|
||||
"^#.+",
|
||||
@@ -154,7 +156,6 @@ const preprocess = (input, { filepath, printWidth }) => {
|
||||
"^(@?)lit(.*)$",
|
||||
"\\.css$",
|
||||
"^@goauthentik/api$",
|
||||
"^[./]",
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
73
packages/prettier-config/package-lock.json
generated
73
packages/prettier-config/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/prettier-config",
|
||||
"version": "3.0.1",
|
||||
"version": "3.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/prettier-config",
|
||||
"version": "3.0.1",
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format-imports": "^4.0.7"
|
||||
@@ -181,9 +181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
@@ -228,9 +228,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
|
||||
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -261,18 +261,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/tsconfig": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/tsconfig/-/tsconfig-1.0.4.tgz",
|
||||
@@ -397,9 +385,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz",
|
||||
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==",
|
||||
"version": "24.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
|
||||
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -701,18 +689,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.30.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
|
||||
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.3.0",
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/core": "^0.15.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@eslint/js": "9.31.0",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -1413,9 +1401,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1463,9 +1451,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
|
||||
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@@ -1478,13 +1466,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-packagejson": {
|
||||
"version": "2.5.16",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.16.tgz",
|
||||
"integrity": "sha512-1EORN4SahAWU55ll+xp0PXhiUmD93PJlBE88GbWv7X5xtZ7ycj3GNbRGX+r75zWn70KAoYVO08rF2C/TqGCHPA==",
|
||||
"version": "2.5.18",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.18.tgz",
|
||||
"integrity": "sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sort-package-json": "3.2.2",
|
||||
"sort-package-json": "3.4.0",
|
||||
"synckit": "0.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -1568,9 +1556,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sort-package-json": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.2.2.tgz",
|
||||
"integrity": "sha512-twAMvmzOcEPsN3N9zKPDpl6zproGU0JcBOQFU4T6e5wrStH8iuPiAjFz9g+cMRC52eQBUbZlFCeGt+F4vginkw==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.4.0.tgz",
|
||||
"integrity": "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1584,6 +1572,9 @@
|
||||
},
|
||||
"bin": {
|
||||
"sort-package-json": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/prettier-config",
|
||||
"version": "3.0.1",
|
||||
"version": "3.1.0",
|
||||
"description": "authentik's Prettier config",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -44,10 +44,11 @@ dependencies = [
|
||||
"kubernetes==33.1.0",
|
||||
"ldap3==2.9.1",
|
||||
"lxml==6.0.0",
|
||||
"msgraph-sdk==1.37.0",
|
||||
"msgraph-sdk==1.38.0",
|
||||
"opencontainers==0.0.15",
|
||||
"packaging==25.0",
|
||||
"paramiko==3.5.1",
|
||||
"psutil==7.0.0",
|
||||
"psycopg[c,pool]==3.2.9",
|
||||
"pydantic==2.11.7",
|
||||
"pydantic-scim==0.0.8",
|
||||
@@ -57,13 +58,13 @@ dependencies = [
|
||||
"pyyaml==6.0.2",
|
||||
"requests-oauthlib==2.0.0",
|
||||
"scim2-filter-parser==0.7.0",
|
||||
"sentry-sdk==2.32.0",
|
||||
"sentry-sdk==2.33.0",
|
||||
"service-identity==24.2.0",
|
||||
"setproctitle==1.3.6",
|
||||
"structlog==25.4.0",
|
||||
"swagger-spec-validator==3.0.4",
|
||||
"tenant-schemas-celery==3.0.0",
|
||||
"twilio==9.6.4",
|
||||
"twilio==9.6.5",
|
||||
"ua-parser==1.0.1",
|
||||
"unidecode==1.4.0",
|
||||
"urllib3<3",
|
||||
@@ -71,7 +72,7 @@ dependencies = [
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.6.0",
|
||||
"wsproto==1.2.0",
|
||||
"xmlsec==1.3.15",
|
||||
"xmlsec==1.3.16",
|
||||
"zxcvbn==4.5.0",
|
||||
]
|
||||
|
||||
@@ -150,6 +151,7 @@ skip = [
|
||||
"./gen-go-api",
|
||||
"*.api.mdx",
|
||||
"./htmlcov",
|
||||
"./media",
|
||||
]
|
||||
dictionary = ".github/codespell-dictionary.txt,-"
|
||||
ignore-words = ".github/codespell-words.txt"
|
||||
|
||||
53
schema.yml
53
schema.yml
@@ -7006,6 +7006,34 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/support_bundle/:
|
||||
post:
|
||||
operationId: enterprise_support_bundle_create
|
||||
description: Generate a support bundle.
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/gzip:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/events/events/:
|
||||
get:
|
||||
operationId: events_events_list
|
||||
@@ -55232,6 +55260,9 @@ components:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
external_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -55296,6 +55327,9 @@ components:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
external_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
@@ -56713,6 +56747,7 @@ components:
|
||||
enum:
|
||||
- apple
|
||||
- openidconnect
|
||||
- entraid
|
||||
- azuread
|
||||
- discord
|
||||
- facebook
|
||||
@@ -58946,6 +58981,8 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
external_id:
|
||||
type: string
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -58960,9 +58997,9 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- external_id
|
||||
- group
|
||||
- group_obj
|
||||
- id
|
||||
- source
|
||||
SCIMSourceGroupRequest:
|
||||
type: object
|
||||
@@ -58971,6 +59008,9 @@ components:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
external_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -58981,8 +59021,8 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- external_id
|
||||
- group
|
||||
- id
|
||||
- source
|
||||
SCIMSourcePropertyMapping:
|
||||
type: object
|
||||
@@ -59089,6 +59129,8 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
external_id:
|
||||
type: string
|
||||
user:
|
||||
type: integer
|
||||
user_obj:
|
||||
@@ -59102,7 +59144,7 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- id
|
||||
- external_id
|
||||
- source
|
||||
- user
|
||||
- user_obj
|
||||
@@ -59113,6 +59155,9 @@ components:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
external_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
@@ -59122,7 +59167,7 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- id
|
||||
- external_id
|
||||
- source
|
||||
- user
|
||||
SMSDevice:
|
||||
|
||||
191
uv.lock
generated
191
uv.lock
generated
@@ -13,7 +13,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.12.13"
|
||||
version = "3.12.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -24,25 +24,25 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload-time = "2025-06-14T15:14:30.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload-time = "2025-06-14T15:14:32.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload-time = "2025-06-14T15:14:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload-time = "2025-06-14T15:14:36.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload-time = "2025-06-14T15:14:38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload-time = "2025-06-14T15:14:39.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload-time = "2025-06-14T15:14:42.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload-time = "2025-06-14T15:14:44.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload-time = "2025-06-14T15:14:45.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload-time = "2025-06-14T15:14:47.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload-time = "2025-06-14T15:14:50.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload-time = "2025-06-14T15:14:52.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload-time = "2025-06-14T15:14:54.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload-time = "2025-06-14T15:14:56.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload-time = "2025-06-14T15:14:58.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload-time = "2025-06-14T15:15:00.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload-time = "2025-06-14T15:15:02.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252, upload-time = "2025-07-10T13:04:26.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514, upload-time = "2025-07-10T13:04:28.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586, upload-time = "2025-07-10T13:04:30.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958, upload-time = "2025-07-10T13:04:32.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287, upload-time = "2025-07-10T13:04:34.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990, upload-time = "2025-07-10T13:04:36.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015, upload-time = "2025-07-10T13:04:38.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678, upload-time = "2025-07-10T13:04:41.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274, upload-time = "2025-07-10T13:04:43.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408, upload-time = "2025-07-10T13:04:45.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879, upload-time = "2025-07-10T13:04:47.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770, upload-time = "2025-07-10T13:04:49.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688, upload-time = "2025-07-10T13:04:51.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -138,11 +138,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.9.0"
|
||||
version = "3.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/68/fb4fb78c9eac59d5e819108a57664737f855c5a8e9b76aec1738bb137f9e/asgiref-3.9.0.tar.gz", hash = "sha256:3dd2556d0f08c4fab8a010d9ab05ef8c34565f6bf32381d17505f7ca5b273767", size = 36772, upload-time = "2025-07-03T13:25:01.491Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f9/76c9f4d4985b5a642926162e2d41fe6019b1fa929cfa58abb7d2dc9041e5/asgiref-3.9.0-py3-none-any.whl", hash = "sha256:06a41250a0114d2b6f6a2cb3ab962147d355b53d1de15eebc34a9d04a7b79981", size = 23788, upload-time = "2025-07-03T13:24:59.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -211,6 +211,7 @@ dependencies = [
|
||||
{ name = "opencontainers" },
|
||||
{ name = "packaging" },
|
||||
{ name = "paramiko" },
|
||||
{ name = "psutil" },
|
||||
{ name = "psycopg", extra = ["c", "pool"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-scim" },
|
||||
@@ -306,10 +307,11 @@ requires-dist = [
|
||||
{ name = "kubernetes", specifier = "==33.1.0" },
|
||||
{ name = "ldap3", specifier = "==2.9.1" },
|
||||
{ name = "lxml", specifier = "==6.0.0" },
|
||||
{ name = "msgraph-sdk", specifier = "==1.37.0" },
|
||||
{ name = "msgraph-sdk", specifier = "==1.38.0" },
|
||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
{ name = "paramiko", specifier = "==3.5.1" },
|
||||
{ name = "psutil", specifier = "==7.0.0" },
|
||||
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.2.9" },
|
||||
{ name = "pydantic", specifier = "==2.11.7" },
|
||||
{ name = "pydantic-scim", specifier = "==0.0.8" },
|
||||
@@ -319,13 +321,13 @@ requires-dist = [
|
||||
{ name = "pyyaml", specifier = "==6.0.2" },
|
||||
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
||||
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.32.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.33.0" },
|
||||
{ name = "service-identity", specifier = "==24.2.0" },
|
||||
{ name = "setproctitle", specifier = "==1.3.6" },
|
||||
{ name = "structlog", specifier = "==25.4.0" },
|
||||
{ name = "swagger-spec-validator", specifier = "==3.0.4" },
|
||||
{ name = "tenant-schemas-celery", specifier = "==3.0.0" },
|
||||
{ name = "twilio", specifier = "==9.6.4" },
|
||||
{ name = "twilio", specifier = "==9.6.5" },
|
||||
{ name = "ua-parser", specifier = "==1.0.1" },
|
||||
{ name = "unidecode", specifier = "==1.4.0" },
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
@@ -333,7 +335,7 @@ requires-dist = [
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.6.0" },
|
||||
{ name = "wsproto", specifier = "==1.2.0" },
|
||||
{ name = "xmlsec", specifier = "==1.3.15" },
|
||||
{ name = "xmlsec", specifier = "==1.3.16" },
|
||||
{ name = "zxcvbn", specifier = "==4.5.0" },
|
||||
]
|
||||
|
||||
@@ -464,7 +466,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "azure-identity"
|
||||
version = "1.23.0"
|
||||
version = "1.23.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-core" },
|
||||
@@ -473,9 +475,9 @@ dependencies = [
|
||||
{ name = "msal-extensions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload-time = "2025-05-14T00:18:30.408Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload-time = "2025-05-14T00:18:32.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -574,30 +576,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.39.3"
|
||||
version = "1.39.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/42/712a74bb86d06538c55067a35b8a82c57aa303eba95b2b1ee91c829288f4/boto3-1.39.3.tar.gz", hash = "sha256:0a367106497649ae3d8a7b571b8c3be01b7b935a0fe303d4cc2574ed03aecbb4", size = 111838, upload-time = "2025-07-03T19:26:00.988Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/43/81ae62386917fa163f1d3bc59e77da56924f9824b5100fd2807e74a675fc/boto3-1.39.7.tar.gz", hash = "sha256:28daeb005e3381808e0e12995056ee8951056ddd43506c07482a15b40ae785b0", size = 111820, upload-time = "2025-07-16T16:29:52.858Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/70/723d2ab259aeaed6c96e5c1857ebe7d474ed9aa8f487dea352c60f33798f/boto3-1.39.3-py3-none-any.whl", hash = "sha256:056cfa2440fe1a157a7c2be897c749c83e1a322144aa4dad889f2fca66571019", size = 139906, upload-time = "2025-07-03T19:25:58.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/60/1d2d708f5bc125fe6fe262ea767c82020ea6deeda113e989cff5ab89a9f9/boto3-1.39.7-py3-none-any.whl", hash = "sha256:c8c0e11fff7bb85f903b860b6bfd4f509a4d749decf38bb6a409ffe5d6eb0c91", size = 139882, upload-time = "2025-07-16T16:29:51.176Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.39.3"
|
||||
version = "1.39.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/66/96e89cc261d75f0b8125436272c335c74d2a39df84504a0c3956adcd1301/botocore-1.39.3.tar.gz", hash = "sha256:da8f477e119f9f8a3aaa8b3c99d9c6856ed0a243680aa3a3fbbfc15a8d4093fb", size = 14132316, upload-time = "2025-07-03T19:25:49.502Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/9d/73f300c841a3c47d2baf4bf6ecb9d6de476edf91de8c3c26ceed5044e666/botocore-1.39.7.tar.gz", hash = "sha256:431e342ef97ecb387cea9df1ae8c4e0edc1b0c9c50d2e121cca77699f24f8dc1", size = 14201331, upload-time = "2025-07-16T16:29:43.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/e4/3698dbb037a44d82a501577c6e3824c19f4289f4afbcadb06793866250d8/botocore-1.39.3-py3-none-any.whl", hash = "sha256:66a81cfac18ad5e9f47696c73fdf44cdbd8f8ca51ab3fca1effca0aabf61f02f", size = 13791724, upload-time = "2025-07-03T19:25:44.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/20/ab70de7441cbc4b8ffc5d6d9a8e02e4c703cc07accb7441ce54020e20950/botocore-1.39.7-py3-none-any.whl", hash = "sha256:1d11ba9f3cb46856bb541ed010db160093201a224d21ef854249513ae3af7e77", size = 13864168, upload-time = "2025-07-16T16:29:37.114Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -667,11 +669,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.6.15"
|
||||
version = "2025.7.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1933,7 +1935,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "microsoft-kiota-authentication-azure"
|
||||
version = "1.9.3"
|
||||
version = "1.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1942,14 +1944,14 @@ dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/cf/d51a8274de4a2113f6acf920b3ec9260368f04da52bdc26b1e26f1e997cd/microsoft_kiota_authentication_azure-1.9.3.tar.gz", hash = "sha256:fcab4d81e58f636aca147f589637ceb17da8bc85d5d22f890b0244cc74a6009d", size = 4985, upload-time = "2025-03-24T16:34:08.271Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/70/02105d0f41d5742084f31226fa77e17072f5551161921b9f4a21a20c6270/microsoft_kiota_authentication_azure-1.9.4.tar.gz", hash = "sha256:a37174072265e17ea05020bf411f4e8694e5335a3aab9bb48926eb1f6ffd263d", size = 4986, upload-time = "2025-06-27T16:04:40.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/38/89420a0f5417a1bf5603de2e74a8981da79fcf1da456c0be9f7cff9c2864/microsoft_kiota_authentication_azure-1.9.3-py3-none-any.whl", hash = "sha256:bccfd312b70fd2b5222a06125022ebf6178d23333b6a594397c8d852a4efd7eb", size = 6905, upload-time = "2025-03-24T16:34:07.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/26/c2fc24eb5ab5e71dd999ec0c653d23d4d2f2cb2af94ad45d488c044b18d9/microsoft_kiota_authentication_azure-1.9.4-py3-none-any.whl", hash = "sha256:ed2f1bb3bbe165141d146f893e790bba8ded09e1001166e74e8b86d7e798ed46", size = 6908, upload-time = "2025-06-27T16:04:38.991Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "microsoft-kiota-http"
|
||||
version = "1.9.3"
|
||||
version = "1.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx", extra = ["http2"] },
|
||||
@@ -1957,9 +1959,9 @@ dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/73/bc8a49643e2cea65fd12cbe61e52be8f142b69e2be143c67219f52c35a08/microsoft_kiota_http-1.9.3.tar.gz", hash = "sha256:e4cda140a362cc304c6ca7495f88b48ae9a4b7d98fa01f5b278e52f3b502ea76", size = 21199, upload-time = "2025-03-24T16:34:16.237Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/a6/0746c11311ec6e768965bf100b89d768bce46d4b04e62a3d6cc9d57d58ea/microsoft_kiota_http-1.9.4.tar.gz", hash = "sha256:31e1d1d3686de66da2e4f89ce5c5bf58bc0041da1375798a07c95f785608a869", size = 21236, upload-time = "2025-06-27T16:04:47.758Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/f4/ef67b7e6dea7d77930df41f4a5926bbba2e362c28a46615908f2d00cf146/microsoft_kiota_http-1.9.3-py3-none-any.whl", hash = "sha256:9a5e2b7a96524b2978e01087f415d810d6bcad7cd866d6d60595ebab2e47c56c", size = 31508, upload-time = "2025-03-24T16:34:15.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/93/ef990609c7b3ba3dbecbe294dbdf1eb4e26005835c4c75e5cb2f9c442581/microsoft_kiota_http-1.9.4-py3-none-any.whl", hash = "sha256:66636be391551eb2fe947f842e4ac76832eda9822b29bf33a8aed527a9511834", size = 31548, upload-time = "2025-06-27T16:04:46.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2071,7 +2073,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-sdk"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-identity" },
|
||||
@@ -2081,9 +2083,9 @@ dependencies = [
|
||||
{ name = "microsoft-kiota-serialization-text" },
|
||||
{ name = "msgraph-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/97/c3da48fa964afa07f5dd644d0fca57dbf6bb6a254c6741e10e1fb1552195/msgraph_sdk-1.37.0.tar.gz", hash = "sha256:92ccafe9f3d10f92e3d24794ff77924e4cc4f03202278595d3221f8cf1220d2f", size = 6001375, upload-time = "2025-07-08T12:33:35.195Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/4a/1231a710be05849b8f2c2ce484dcc5a84066bbbc01e8e2c0e7b812a8909c/msgraph_sdk-1.38.0.tar.gz", hash = "sha256:4bb5b30515e64de1e507641f923a348ce83bacb52e2703bb941c144a319c4ca7", size = 6085852, upload-time = "2025-07-17T01:14:36.377Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/22/8985a3fee5ed411f12775541898b2a9d470afd230419b709b5318da84556/msgraph_sdk-1.37.0-py3-none-any.whl", hash = "sha256:e51580cb12b78984280cf3094892e254f34405c9bd6450931b0baa91d7dad8f6", size = 24606234, upload-time = "2025-07-08T12:33:31.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/79/a97b43015afd7112b6f6b0ec1cc65fd06d4f0a9f6737427ba6faf1d5aed4/msgraph_sdk-1.38.0-py3-none-any.whl", hash = "sha256:a6c4725085323152581ed9c9b3433b3f4b6a2e3365a2674354c78a72da8bce9a", size = 24989016, upload-time = "2025-07-17T01:14:32.033Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2165,65 +2167,65 @@ source = { git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.34.1"
|
||||
version = "1.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.34.1"
|
||||
version = "1.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/1eb2ed2ce55e0a9aa95b3007f26f55c7943aeef0a783bb006bdd92b3299e/opentelemetry_sdk-1.35.0.tar.gz", hash = "sha256:2a400b415ab68aaa6f04e8a6a9f6552908fb3090ae2ff78d6ae0c597ac581954", size = 160871, upload-time = "2025-07-11T12:23:39.566Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4f/8e32b757ef3b660511b638ab52d1ed9259b666bdeeceba51a082ce3aea95/opentelemetry_sdk-1.35.0-py3-none-any.whl", hash = "sha256:223d9e5f5678518f4842311bb73966e0b6db5d1e0b74e35074c052cd2487f800", size = 119379, upload-time = "2025-07-11T12:23:24.521Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.55b1"
|
||||
version = "0.56b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/8e/214fa817f63b9f068519463d8ab46afd5d03b98930c39394a37ae3e741d0/opentelemetry_semantic_conventions-0.56b0.tar.gz", hash = "sha256:c114c2eacc8ff6d3908cb328c811eaf64e6d68623840be9224dc829c4fd6c2ea", size = 124221, upload-time = "2025-07-11T12:23:40.71Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3f/e80c1b017066a9d999efffe88d1cce66116dcf5cb7f80c41040a83b6e03b/opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2", size = 201625, upload-time = "2025-07-11T12:23:25.63Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.18"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/87/03ababa86d984952304ac8ce9fbd3a317afb4a225b9a81f9b606ac60c873/orjson-3.11.0.tar.gz", hash = "sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700", size = 5318246, upload-time = "2025-07-15T16:08:29.194Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087, upload-time = "2025-04-29T23:29:19.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273, upload-time = "2025-04-29T23:29:20.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779, upload-time = "2025-04-29T23:29:22.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811, upload-time = "2025-04-29T23:29:23.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018, upload-time = "2025-04-29T23:29:25.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368, upload-time = "2025-04-29T23:29:26.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840, upload-time = "2025-04-29T23:29:28.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135, upload-time = "2025-04-29T23:29:29.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810, upload-time = "2025-04-29T23:29:31.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491, upload-time = "2025-04-29T23:29:33.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277, upload-time = "2025-04-29T23:29:34.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367, upload-time = "2025-04-29T23:29:36.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/63/82d9b6b48624009d230bc6038e54778af8f84dfd54402f9504f477c5cfd5/orjson-3.11.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb", size = 240125, upload-time = "2025-07-15T16:07:35.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3a/d557ed87c63237d4c97a7bac7ac054c347ab8c4b6da09748d162ca287175/orjson-3.11.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f", size = 129189, upload-time = "2025-07-15T16:07:37.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5e/b2c9e22e2cd10aa7d76a629cee65d661e06a61fbaf4dc226386f5636dd44/orjson-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d", size = 131953, upload-time = "2025-07-15T16:07:39.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/60/760fcd9b50eb44d1206f2b30c8d310b79714553b9d94a02f9ea3252ebe63/orjson-3.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246", size = 126922, upload-time = "2025-07-15T16:07:41.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/8c46daa867ccc92da6de9567608be62052774b924a77c78382e30d50b579/orjson-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8", size = 128787, upload-time = "2025-07-15T16:07:42.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/14/a2f1b123d85f11a19e8749f7d3f9ed6c9b331c61f7b47cfd3e9a1fedb9bc/orjson-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51", size = 131895, upload-time = "2025-07-15T16:07:44.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/10/362e8192df7528e8086ea712c5cb01355c8d4e52c59a804417ba01e2eb2d/orjson-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e", size = 133868, upload-time = "2025-07-15T16:07:46.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4e/ef43582ef3e3dfd2a39bc3106fa543364fde1ba58489841120219da6e22f/orjson-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3", size = 128234, upload-time = "2025-07-15T16:07:48.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/fa/02dabb2f1d605bee8c4bb1160cfc7467976b1ed359a62cc92e0681b53c45/orjson-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4", size = 130232, upload-time = "2025-07-15T16:07:50.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/76/951b5619605c8d2ede80cc989f32a66abc954530d86e84030db2250c63a1/orjson-3.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4", size = 403648, upload-time = "2025-07-15T16:07:52.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e2/5fa53bb411455a63b3713db90b588e6ca5ed2db59ad49b3fb8a0e94e0dda/orjson-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486", size = 144572, upload-time = "2025-07-15T16:07:54.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/d0/7d6f91e1e0f034258c3a3358f20b0c9490070e8a7ab8880085547274c7f9/orjson-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836", size = 132766, upload-time = "2025-07-15T16:07:55.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f8/4d46481f1b3fb40dc826d62179f96c808eb470cdcc74b6593fb114d74af3/orjson-3.11.0-cp313-cp313-win32.whl", hash = "sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132", size = 134638, upload-time = "2025-07-15T16:07:57.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/3f/544938dcfb7337d85ee1e43d7685cf8f3bfd452e0b15a32fe70cb4ca5094/orjson-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6", size = 129411, upload-time = "2025-07-15T16:07:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/0c/f75015669d7817d222df1bb207f402277b77d22c4833950c8c8c7cf2d325/orjson-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a", size = 126349, upload-time = "2025-07-15T16:08:00.322Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2411,6 +2413,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.2.9"
|
||||
@@ -2964,15 +2981,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.32.0"
|
||||
version = "2.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/0b/6139f589436c278b33359845ed77019cd093c41371f898283bbc14d26c02/sentry_sdk-2.33.0.tar.gz", hash = "sha256:cdceed05e186846fdf80ceea261fe0a11ebc93aab2f228ed73d076a07804152e", size = 335233, upload-time = "2025-07-15T12:07:42.413Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e5/f24e9f81c9822a24a2627cfcb44c10a3971382e67e5015c6e068421f5787/sentry_sdk-2.33.0-py2.py3-none-any.whl", hash = "sha256:a762d3f19a1c240e16c98796f2a5023f6e58872997d5ae2147ac3ed378b23ec2", size = 356397, upload-time = "2025-07-15T12:07:40.729Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3185,7 +3202,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.6.4"
|
||||
version = "9.6.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -3193,9 +3210,9 @@ dependencies = [
|
||||
{ name = "pyjwt" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/c6/46d00a9a4082f9ecb70da6643369e8b0bee61ffa35026f2df97f311971ce/twilio-9.6.4.tar.gz", hash = "sha256:879a6d2d93a52539660e59c2e49908ab5d51cf1284de7bb097cd129d7d9ad73c", size = 1038231, upload-time = "2025-07-03T09:50:32.018Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/5e/0a591781cd360819f61ccb8337efbc5d9931cf286e5b13e5d3ca72735e06/twilio-9.6.5.tar.gz", hash = "sha256:4f246b3d3474090b55dc13034b7cec2f9250877f5298f8419a3fe8a447f36f71", size = 1037726, upload-time = "2025-07-10T11:59:39.64Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/02/71683ebf7cb68afc1eb8a191b8d6f85887d884c2a52f833eddd7dd22faaa/twilio-9.6.4-py2.py3-none-any.whl", hash = "sha256:3eca01998bdd2f105d3b46fcbd316d839660e898c704899f26e4182567ffd3d1", size = 1895325, upload-time = "2025-07-03T09:50:30.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/89/b32b7c3afef8635f94d2b9a89ae95adf2f13c942c3af047ccaa6d0c442f3/twilio-9.6.5-py2.py3-none-any.whl", hash = "sha256:e9f547267b59fb474bfc715c042662d9d2b1a1bbcde34017954407aa95482277", size = 1895395, upload-time = "2025-07-10T11:59:37.619Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3492,16 +3509,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "xmlsec"
|
||||
version = "1.3.15"
|
||||
version = "1.3.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/0b/d851367799b865500efd0b255c39fc5d30892ea28c1569ca185a76d19576/xmlsec-1.3.15.tar.gz", hash = "sha256:baa856b83d0012e278e6f6cbec96ac8128de667ca9fa9a2eeb02c752e816f6d8", size = 114117, upload-time = "2025-03-11T22:37:00.567Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/52/b025fe78a178d9eaffa1c77a4a73b20072e27d4679200cee76475034b399/xmlsec-1.3.16.tar.gz", hash = "sha256:2b6c70544c6d1d4ca006aaa314958e0ef3514dc81fffde1b23f2ec41a5791f9d", size = 114202, upload-time = "2025-07-10T12:45:37.847Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/17/0a272e6087ddb24bec96528acf061341845f458671e2a5cb35ff867a7c89/xmlsec-1.3.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ac2154311d32a6571e22f224ed16356029e59bd5ca76edeb3922a809adfe89c", size = 3746315, upload-time = "2025-03-11T22:36:43.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/91/7ce9317e3a2a03e3811e62be52e091c1e661da2d59b5c7f60ec1840a1e6b/xmlsec-1.3.15-cp313-cp313-win32.whl", hash = "sha256:5ed218129f89b0592926ad2be42c017bece469db9b7380dc41bc09b01ca26d5d", size = 2146158, upload-time = "2025-03-11T22:36:44.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/93311b9eedc11055ba667e666dc6ca1e2cc59c2356e91b73c3d5a6738fbf/xmlsec-1.3.15-cp313-cp313-win_amd64.whl", hash = "sha256:5fc29e69b064323317b3862751a3a8107670e0a17510ca4517bbdc1939a90b1a", size = 2442027, upload-time = "2025-03-11T22:36:46.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/72/dae0618ff7a86dada22bf0ae16730a48b98e513fa8098681156e74dd687c/xmlsec-1.3.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68b60867e6be61e06964be64d7501b88156f0ad0a4808b5c2eb8b16d8de9b6bc", size = 3424682, upload-time = "2025-07-10T12:18:39.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/0a/6e1d9108f0b0801f874a153256ec044d1b1c53713517f8f24ff5c21544e5/xmlsec-1.3.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:174917827e26ff71dab512e5e87544dcd3309eb4a5dc18e6f1e8022789e1f1bf", size = 3830253, upload-time = "2025-07-10T12:18:41.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/35/0d11fceb1978564a14c95b687a08a27b96873aeb54d9c1a5c90efd824766/xmlsec-1.3.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb683689a7770a3a71657af96c1953c9d00da7e3c8d9a9851777fa2c9ecf741a", size = 4367122, upload-time = "2025-07-10T12:18:42.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/ad52185d08fd35ae323124c0a727697d2535e1794c864f4858e1b61eb7bf/xmlsec-1.3.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fda1627987e26989ead9ae8ec8b089f2affa0e922f585ab1c37d1c2111c78c7", size = 3791602, upload-time = "2025-07-10T12:18:44.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/fe/867975dd2a20640882282788f66c535d746cce01265a2732bde596a98cf7/xmlsec-1.3.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4fe9e8ab3497632ea242fc718a57e7524e0300537fd106a7455696b339ac0a6", size = 4098004, upload-time = "2025-07-10T12:18:45.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/35/66f717c4ff288c61c2e447fdc453fa21446d15e39ae756a129a3aa1bd4ae/xmlsec-1.3.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1014c6cb723ad99781a2f37c03969eb246b310da029ddf3250df998801bb62d7", size = 4400559, upload-time = "2025-07-10T12:18:47.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/9c/2e6ee7f9f313b4848b2f9dac51fbf6615932f1aa6b9399d17ab29c2bd731/xmlsec-1.3.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:795eedbdb4dbc72c786dbf4193561de0f3ebd8f8ee3a32e81b085b24ccbee120", size = 4153379, upload-time = "2025-07-10T12:18:48.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f4/08da3d448456e4c41ddc99a0e713313b774beed5a27689b066d47d114194/xmlsec-1.3.16-cp313-cp313-win32.whl", hash = "sha256:1f83fbd4c44bbeebd19db9de5969b68ed7d5b723d7812c5a62c795452f7c8945", size = 2155805, upload-time = "2025-07-10T12:18:51.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/80/8ffe311f8e2273bb156b4cb0f2454e6a089723575921c4c07c24dd1aa3ae/xmlsec-1.3.16-cp313-cp313-win_amd64.whl", hash = "sha256:4b8b36cdccd13fa84a923958ebf5f73febe8a97da4ecff711fdeb97fec6896db", size = 2446038, upload-time = "2025-07-10T12:18:50.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
18
web/.storybook/DocsContainer.tsx
Normal file
18
web/.storybook/DocsContainer.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { extendStorybookTheme } from "./theme.js";
|
||||
|
||||
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
import { DocsContainer, DocsContainerProps } from "@storybook/addon-docs/blocks";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export const ThemedDocsContainer: React.FC<DocsContainerProps> = ({
|
||||
theme: initialTheme = resolveUITheme(),
|
||||
...props
|
||||
}) => {
|
||||
const [theme, setTheme] = useState(initialTheme);
|
||||
const resolvedTheme = useMemo(() => extendStorybookTheme(theme), [theme]);
|
||||
|
||||
useEffect(() => createUIThemeEffect(setTheme), []);
|
||||
|
||||
return <DocsContainer {...props} theme={resolvedTheme} />;
|
||||
};
|
||||
@@ -3,8 +3,8 @@
|
||||
* @import { StorybookConfig } from "@storybook/web-components-vite";
|
||||
* @import { InlineConfig, Plugin } from "vite";
|
||||
*/
|
||||
|
||||
import postcssLit from "rollup-plugin-postcss-lit";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
|
||||
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
||||
@@ -27,11 +27,6 @@ const inlineCSSPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @satisfies {InlineConfig}
|
||||
*/
|
||||
// const viteFinal = ;
|
||||
|
||||
/**
|
||||
* @satisfies {StorybookConfig}
|
||||
*/
|
||||
@@ -42,18 +37,11 @@ const config = {
|
||||
{ from: "../authentik", to: "/static/authentik" },
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-controls",
|
||||
// ---
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-addon-mock",
|
||||
"@storybook/addon-docs",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/web-components-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
framework: "@storybook/web-components-vite",
|
||||
async viteFinal(config) {
|
||||
const [{ mergeConfig }, { createBundleDefinitions }] = await Promise.all([
|
||||
import("vite"),
|
||||
@@ -65,10 +53,11 @@ const config = {
|
||||
*/
|
||||
const overrides = {
|
||||
define: createBundleDefinitions(),
|
||||
plugins: [inlineCSSPlugin, postcssLit(), tsconfigPaths()],
|
||||
plugins: [inlineCSSPlugin, postcssLit()],
|
||||
};
|
||||
|
||||
return mergeConfig(config, overrides);
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
/**
|
||||
* @file Storybook manager configuration.
|
||||
*
|
||||
* @import { ThemeVarsPartial } from "storybook/internal/theming";
|
||||
*/
|
||||
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
||||
import { addons } from "@storybook/manager-api";
|
||||
import { create } from "@storybook/theming/create";
|
||||
|
||||
/**
|
||||
* @satisfies {Partial<ThemeVarsPartial>}
|
||||
*/
|
||||
const baseTheme = {
|
||||
brandTitle: "authentik Storybook",
|
||||
brandUrl: "https://goauthentik.io",
|
||||
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
|
||||
brandTarget: "_self",
|
||||
};
|
||||
import { extendStorybookTheme } from "./theme.js";
|
||||
|
||||
const uiTheme = resolveUITheme();
|
||||
import { createUIThemeEffect } from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
...baseTheme,
|
||||
base: uiTheme,
|
||||
}),
|
||||
enableShortcuts: false,
|
||||
});
|
||||
import { addons } from "storybook/manager-api";
|
||||
|
||||
createUIThemeEffect((nextUITheme) => {
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
...baseTheme,
|
||||
base: nextUITheme,
|
||||
}),
|
||||
theme: extendStorybookTheme(nextUITheme),
|
||||
enableShortcuts: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<style>
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sb-main-padded {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.story-shadow-container {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0.25em #ddd;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
margin: 0 auto;
|
||||
max-width: 72em;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.docs-story .story-shadow-container {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.story-shadow-container[display-mode="flex-wrap"] {
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin: 2rem -1rem 1rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.title code {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: bold;
|
||||
padding: 0.1rem 0.25rem;
|
||||
}
|
||||
|
||||
.sbdocs-preview .hljs {
|
||||
color: #fff !important;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.sbdocs-pre > div {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,36 @@
|
||||
/// <reference types="../types/css.js" />
|
||||
/**
|
||||
* @file Storybook manager configuration.
|
||||
*
|
||||
* @import { Preview } from "@storybook/web-components";
|
||||
*/
|
||||
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
applyDocumentTheme();
|
||||
import "#common/styles/storybook.css";
|
||||
|
||||
import { ThemedDocsContainer } from "./DocsContainer.tsx";
|
||||
import { extendStorybookTheme } from "./theme.js";
|
||||
|
||||
import {
|
||||
applyDocumentTheme,
|
||||
createUIThemeEffect,
|
||||
resolveUITheme,
|
||||
} from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
const base = resolveUITheme();
|
||||
const theme = extendStorybookTheme(base);
|
||||
|
||||
createUIThemeEffect(applyDocumentTheme);
|
||||
|
||||
/**
|
||||
* @satisfies {Preview}
|
||||
*/
|
||||
const preview = {
|
||||
tags: ["autodocs"],
|
||||
|
||||
parameters: {
|
||||
docs: {
|
||||
theme,
|
||||
container: ThemedDocsContainer,
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
method: "alphabetical",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user