Compare commits
11 Commits
website/in
...
events/imp
Author | SHA1 | Date | |
---|---|---|---|
1fcef476c3 | |||
e8b6b3366b | |||
f76becfd86 | |||
080e2311fe | |||
eacc0eb546 | |||
c77a54dc2a | |||
84781df51b | |||
a640866534 | |||
e070241407 | |||
85985c3673 | |||
3abe6cd02c |
@ -21,6 +21,8 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:package.json]
|
||||
|
||||
[bumpversion:file:package-lock.json]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
@ -31,6 +33,4 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:web/src/common/constants.ts]
|
||||
|
||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
||||
|
4
Makefile
4
Makefile
@ -86,6 +86,10 @@ dev-create-db:
|
||||
|
||||
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
|
||||
|
||||
update-test-mmdb: ## Update test GeoIP and ASN Databases
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
|
||||
|
||||
#########################
|
||||
## API Schema
|
||||
#########################
|
||||
|
@ -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:
|
||||
|
@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
|
||||
self.reader: Reader | None = None
|
||||
self._last_mtime: float = 0.0
|
||||
self.logger = get_logger()
|
||||
self.open()
|
||||
self.load()
|
||||
|
||||
def path(self) -> str | None:
|
||||
"""Get the path to the MMDB file to load"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open(self):
|
||||
def load(self):
|
||||
"""Get GeoIP Reader, if configured, otherwise none"""
|
||||
path = self.path()
|
||||
if path == "" or not path:
|
||||
@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
|
||||
diff = self._last_mtime < mtime
|
||||
if diff > 0:
|
||||
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
|
||||
self.open()
|
||||
self.load()
|
||||
except OSError as exc:
|
||||
self.logger.warning("Failed to check MMDB age", exc=exc)
|
||||
|
||||
|
@ -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 before_send
|
||||
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 before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||
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
|
||||
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
|
||||
@ -238,8 +237,8 @@ class FlowExecutorView(APIView):
|
||||
self._logger.warning(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)))
|
||||
|
@ -237,4 +237,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"])
|
||||
|
@ -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()
|
||||
|
||||
|
@ -28,7 +28,6 @@ from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.sentry import before_send
|
||||
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 before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||
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]:
|
||||
|
@ -11,6 +11,8 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.test.runner import DiscoverRunner
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
|
||||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sentry import sentry_init
|
||||
from authentik.root.signals import post_startup, pre_startup, startup
|
||||
@ -76,6 +78,9 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
||||
for key, value in test_config.items():
|
||||
CONFIG.set(key, value)
|
||||
|
||||
ASN_CONTEXT_PROCESSOR.load()
|
||||
GEOIP_CONTEXT_PROCESSOR.load()
|
||||
|
||||
sentry_init()
|
||||
self.logger.debug("Test environment configured")
|
||||
|
||||
|
@ -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
|
||||
@ -71,37 +70,31 @@ def ldap_sync_single(source_pk: str):
|
||||
return
|
||||
# Delete all sync tasks from the cache
|
||||
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
|
||||
task = chain(
|
||||
# User and group sync can happen at once, they have no dependencies on each other
|
||||
group(
|
||||
ldap_sync_paginator(source, UserLDAPSynchronizer)
|
||||
+ ldap_sync_paginator(source, GroupLDAPSynchronizer),
|
||||
),
|
||||
# Membership sync needs to run afterwards
|
||||
group(
|
||||
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
|
||||
),
|
||||
# Finally, deletions. What we'd really like to do here is something like
|
||||
# ```
|
||||
# user_identifiers = <ldap query>
|
||||
# User.objects.exclude(
|
||||
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
|
||||
# ).delete()
|
||||
# ```
|
||||
# This runs into performance issues in large installations. So instead we spread the
|
||||
# work out into three steps:
|
||||
# 1. Get every object from the LDAP source.
|
||||
# 2. Mark every object as "safe" in the database. This is quick, but any error could
|
||||
# mean deleting users which should not be deleted, so we do it immediately, in
|
||||
# large chunks, and only queue the deletion step afterwards.
|
||||
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
|
||||
# small chunks.
|
||||
group(
|
||||
ldap_sync_paginator(source, UserLDAPForwardDeletion)
|
||||
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
|
||||
),
|
||||
|
||||
# The order of these operations needs to be preserved as each depends on the previous one(s)
|
||||
# 1. User and group sync can happen simultaneously
|
||||
# 2. Membership sync needs to run afterwards
|
||||
# 3. Finally, user and group deletions can happen simultaneously
|
||||
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
|
||||
source, GroupLDAPSynchronizer
|
||||
)
|
||||
task()
|
||||
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
|
||||
user_group_deletion = ldap_sync_paginator(
|
||||
source, UserLDAPForwardDeletion
|
||||
) + ldap_sync_paginator(source, GroupLDAPForwardDeletion)
|
||||
|
||||
# Celery is buggy with empty groups, so we are careful only to add non-empty groups.
|
||||
# See https://github.com/celery/celery/issues/9772
|
||||
task_groups = []
|
||||
if user_group_sync:
|
||||
task_groups.append(group(user_group_sync))
|
||||
if membership_sync:
|
||||
task_groups.append(group(membership_sync))
|
||||
if user_group_deletion:
|
||||
task_groups.append(group(user_group_deletion))
|
||||
|
||||
all_tasks = chain(task_groups)
|
||||
all_tasks()
|
||||
|
||||
|
||||
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:
|
||||
@ -155,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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
|
||||
)
|
||||
if configured_binding_net != NetworkBinding.NO_BINDING:
|
||||
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||
if configured_binding_geo != GeoIPBinding.NO_BINDING:
|
||||
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
||||
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
||||
# If we got to this point without any error being raised, we need to
|
||||
# update the last saved IP to the current one
|
||||
if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
|
||||
@ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
# (== basically requires the user to be logged in)
|
||||
request.session[request.session.model.Keys.LAST_IP] = new_ip
|
||||
|
||||
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
|
||||
@staticmethod
|
||||
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
|
||||
"""Check network/ASN binding"""
|
||||
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
|
||||
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
|
||||
@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
new_ip,
|
||||
)
|
||||
|
||||
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
|
||||
@staticmethod
|
||||
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
|
||||
"""Check GeoIP binding"""
|
||||
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
|
||||
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
|
||||
@ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.continent != new_geo.continent:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.continent",
|
||||
last_geo.continent,
|
||||
new_geo.continent,
|
||||
last_geo.continent.to_dict(),
|
||||
new_geo.continent.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
@ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.country != new_geo.country:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.country",
|
||||
last_geo.country,
|
||||
new_geo.country,
|
||||
last_geo.country.to_dict(),
|
||||
new_geo.country.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
@ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.city != new_geo.city:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.city",
|
||||
last_geo.city,
|
||||
new_geo.city,
|
||||
last_geo.city.to_dict(),
|
||||
new_geo.city.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
|
@ -3,6 +3,7 @@
|
||||
from time import sleep
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
@ -17,7 +18,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
from authentik.stages.user_login.middleware import (
|
||||
BoundSessionMiddleware,
|
||||
SessionBindingBroken,
|
||||
logout_extra,
|
||||
)
|
||||
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
|
||||
|
||||
|
||||
class TestUserLoginStage(FlowTestCase):
|
||||
@ -192,3 +198,52 @@ class TestUserLoginStage(FlowTestCase):
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
response = self.client.get(reverse("authentik_api:application-list"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_binding_net_break_log(self):
|
||||
"""Test logout_extra with exception"""
|
||||
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
|
||||
for args, expect in [
|
||||
[[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
|
||||
[[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
|
||||
[
|
||||
[NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
|
||||
["network.asn_network"],
|
||||
],
|
||||
[[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
|
||||
]:
|
||||
with self.subTest(args[0]):
|
||||
with self.assertRaises(SessionBindingBroken) as cm:
|
||||
BoundSessionMiddleware.recheck_session_net(*args)
|
||||
self.assertEqual(cm.exception.reason, expect[0])
|
||||
# Ensure the request can be logged without throwing errors
|
||||
self.client.force_login(self.user)
|
||||
request = HttpRequest()
|
||||
request.session = self.client.session
|
||||
request.user = self.user
|
||||
logout_extra(request, cm.exception)
|
||||
|
||||
def test_binding_geo_break_log(self):
|
||||
"""Test logout_extra with exception"""
|
||||
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
|
||||
for args, expect in [
|
||||
[[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
|
||||
[[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
|
||||
[
|
||||
[GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
|
||||
["geoip.country"],
|
||||
],
|
||||
[
|
||||
[GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
|
||||
["geoip.city"],
|
||||
],
|
||||
]:
|
||||
with self.subTest(args[0]):
|
||||
with self.assertRaises(SessionBindingBroken) as cm:
|
||||
BoundSessionMiddleware.recheck_session_geo(*args)
|
||||
self.assertEqual(cm.exception.reason, expect[0])
|
||||
# Ensure the request can be logged without throwing errors
|
||||
self.client.force_login(self.user)
|
||||
request = HttpRequest()
|
||||
request.session = self.client.session
|
||||
request.user = self.user
|
||||
logout_extra(request, cm.exception)
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1018.1",
|
||||
"aws-cdk": "^2.1019.1",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1018.1",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz",
|
||||
"integrity": "sha512-kFPRox5kSm+ktJ451o0ng9rD+60p5Kt1CZIWw8kXnvqbsxN2xv6qbmyWSXw7sGVXVwqrRKVj+71/JeDr+LMAZw==",
|
||||
"version": "2.1019.1",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1019.1.tgz",
|
||||
"integrity": "sha512-G2jxKuTsYTrYZX80CDApCrKcZ+AuFxxd+b0dkb0KEkfUsela7RqrDGLm5wOzSCIc3iH6GocR8JDVZuJ+0nNuKg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1018.1",
|
||||
"aws-cdk": "^2.1019.1",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ dependencies = [
|
||||
"flower==2.0.1",
|
||||
"geoip2==5.1.0",
|
||||
"geopy==2.4.1",
|
||||
"google-api-python-client==2.172.0",
|
||||
"google-api-python-client==2.173.0",
|
||||
"gssapi==1.9.0",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonpatch==1.33",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
8
uv.lock
generated
8
uv.lock
generated
@ -298,7 +298,7 @@ requires-dist = [
|
||||
{ name = "flower", specifier = "==2.0.1" },
|
||||
{ name = "geoip2", specifier = "==5.1.0" },
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "google-api-python-client", specifier = "==2.172.0" },
|
||||
{ name = "google-api-python-client", specifier = "==2.173.0" },
|
||||
{ name = "gssapi", specifier = "==1.9.0" },
|
||||
{ name = "gunicorn", specifier = "==23.0.0" },
|
||||
{ name = "jsonpatch", specifier = "==1.33" },
|
||||
@ -1402,7 +1402,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.172.0"
|
||||
version = "2.173.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
@ -1411,9 +1411,9 @@ dependencies = [
|
||||
{ name = "httplib2" },
|
||||
{ name = "uritemplate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/69/c0cec6be5878d4de161f64096edb3d4a2d1a838f036b8425ea8358d0dfb3/google_api_python_client-2.172.0.tar.gz", hash = "sha256:dcb3b7e067154b2aa41f1776cf86584a5739c0ac74e6ff46fc665790dca0e6a6", size = 13074841, upload-time = "2025-06-10T16:58:41.181Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/7e/7c6e43e54f611f0f97f1678ea567fe06fecd545bd574db05e204e5b136fe/google_api_python_client-2.173.0.tar.gz", hash = "sha256:b537bc689758f4be3e6f40d59a6c0cd305abafdea91af4bc66ec31d40c08c804", size = 13091318, upload-time = "2025-06-19T19:39:05.881Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fc/8850ccf21c5df43faeaf8bba8c4149ee880b41b8dc7066e3259bcfd921ca/google_api_python_client-2.172.0-py3-none-any.whl", hash = "sha256:9f1b9a268d5dc1228207d246c673d3a09ee211b41a11521d38d9212aeaa43af7", size = 13595800, upload-time = "2025-06-10T16:58:38.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/c9/dc9ca0537ee2ddac0f0b1e458903afe3f490a0f90dfd4b1b16eb339cdfbb/google_api_python_client-2.173.0-py3-none-any.whl", hash = "sha256:16a8e81c772dd116f5c4ee47d83643149e1367dc8fb4f47cb471fbcb5c7d7ac7", size = 13612778, upload-time = "2025-06-19T19:39:03.283Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
59
web/package-lock.json
generated
59
web/package-lock.json
generated
@ -37,6 +37,7 @@
|
||||
"@sentry/browser": "^9.30.0",
|
||||
"@spotlightjs/spotlight": "^3.0.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
@ -68,7 +69,6 @@
|
||||
"trusted-types": "^2.0.0",
|
||||
"ts-pattern": "^5.7.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webauthn-polyfills": "^0.1.7",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
@ -4768,12 +4768,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@simplewebauthn/types": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz",
|
||||
"integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@ -7361,12 +7355,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||
},
|
||||
"node_modules/@types/ua-parser-js": {
|
||||
"version": "0.7.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
|
||||
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
@ -11989,7 +11977,8 @@
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="
|
||||
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compatx": {
|
||||
"version": "0.1.8",
|
||||
@ -27224,32 +27213,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.40",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
||||
"integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
@ -28599,18 +28562,6 @@
|
||||
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webauthn-polyfills": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/webauthn-polyfills/-/webauthn-polyfills-0.1.7.tgz",
|
||||
"integrity": "sha512-tOA5KPHhN8j8EBA9I90bYmsEc6CAKd1SbWJzmVn0hmTfvfiNJLGGzRPlSW4fKiQPm8BC6doPQC0CnaQdhxsL3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/types": "^11.0.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"compare-versions": "^6.1.1",
|
||||
"ua-parser-js": "^1.0.39"
|
||||
}
|
||||
},
|
||||
"node_modules/webcomponent-qr-code": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/webcomponent-qr-code/-/webcomponent-qr-code-1.2.0.tgz",
|
||||
@ -29535,11 +29486,11 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"base64-js": "^1.5.1",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"weakmap-polyfill": "^2.0.4",
|
||||
"webauthn-polyfills": "^0.1.7"
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
|
@ -108,6 +108,7 @@
|
||||
"@sentry/browser": "^9.30.0",
|
||||
"@spotlightjs/spotlight": "^3.0.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
@ -139,7 +140,6 @@
|
||||
"trusted-types": "^2.0.0",
|
||||
"ts-pattern": "^5.7.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webauthn-polyfills": "^0.1.7",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
|
@ -11,11 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"base64-js": "^1.5.1",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"weakmap-polyfill": "^2.0.4",
|
||||
"webauthn-polyfills": "^0.1.7"
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { fromByteArray } from "base64-js";
|
||||
import "formdata-polyfill";
|
||||
import $ from "jquery";
|
||||
import "weakmap-polyfill";
|
||||
import "webauthn-polyfills";
|
||||
|
||||
import {
|
||||
type AuthenticatorValidationChallenge,
|
||||
@ -257,9 +257,47 @@ class AutosubmitStage extends Stage<AutosubmitChallenge> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Assertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
registrationClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
attestationObject: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthAssertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
assertionClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
authenticatorData: string;
|
||||
signature: string;
|
||||
userHandle: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
b64enc(buf: Uint8Array): string {
|
||||
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
b64RawEnc(buf: Uint8Array): string {
|
||||
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
u8arr(input: string): Uint8Array {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
}
|
||||
|
||||
checkWebAuthnSupport(): boolean {
|
||||
if ("credentials" in navigator) {
|
||||
return true;
|
||||
@ -272,6 +310,98 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
*/
|
||||
transformCredentialCreateOptions(
|
||||
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||
userId: string,
|
||||
): PublicKeyCredentialCreationOptions {
|
||||
const user = credentialCreateOptions.user;
|
||||
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||
// string, then a byte array, re-encode it and wrap that in an array.
|
||||
const stringId = decodeURIComponent(window.atob(userId));
|
||||
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
|
||||
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
|
||||
|
||||
return Object.assign({}, credentialCreateOptions, {
|
||||
challenge,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the binary data in the credential into base64 strings
|
||||
* for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||
const attObj = new Uint8Array(
|
||||
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: this.b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
response: {
|
||||
clientDataJSON: this.b64enc(clientDataJSON),
|
||||
attestationObject: this.b64enc(attObj),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
transformCredentialRequestOptions(
|
||||
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
|
||||
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||
(credentialDescriptor) => {
|
||||
const id = this.u8arr(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
},
|
||||
);
|
||||
|
||||
return Object.assign({}, credentialRequestOptions, {
|
||||
challenge,
|
||||
allowCredentials,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
const sig = new Uint8Array(response.signature);
|
||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: this.b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
|
||||
response: {
|
||||
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
||||
signature: this.b64RawEnc(sig),
|
||||
authenticatorData: this.b64RawEnc(authData),
|
||||
userHandle: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.challenge.deviceChallenges.length === 1) {
|
||||
this.deviceChallenge = this.challenge.deviceChallenges[0];
|
||||
@ -375,8 +505,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
||||
`);
|
||||
navigator.credentials
|
||||
.get({
|
||||
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
|
||||
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptionsJSON,
|
||||
publicKey: this.transformCredentialRequestOptions(
|
||||
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
|
||||
),
|
||||
})
|
||||
.then((assertion) => {
|
||||
@ -384,9 +514,15 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
|
||||
throw new Error("No assertion");
|
||||
}
|
||||
try {
|
||||
// we now have an authentication assertion! encode the byte arrays contained
|
||||
// in the assertion data as strings for posting to the server
|
||||
const transformedAssertionForServer = this.transformAssertionForServer(
|
||||
assertion as PublicKeyCredential,
|
||||
);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
this.executor.submit({
|
||||
webauthn: (assertion as PublicKeyCredential).toJSON(),
|
||||
webauthn: transformedAssertionForServer,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||
|
@ -1,5 +1,21 @@
|
||||
import * as base64js from "base64-js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
export function b64enc(buf: Uint8Array): string {
|
||||
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
export function b64RawEnc(buf: Uint8Array): string {
|
||||
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
export function u8arr(input: string): Uint8Array {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
}
|
||||
|
||||
export function checkWebAuthnSupport() {
|
||||
if ("credentials" in navigator) {
|
||||
return;
|
||||
@ -9,3 +25,121 @@ export function checkWebAuthnSupport() {
|
||||
}
|
||||
throw new Error(msg("WebAuthn not supported by browser."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
*/
|
||||
export function transformCredentialCreateOptions(
|
||||
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||
userId: string,
|
||||
): PublicKeyCredentialCreationOptions {
|
||||
const user = credentialCreateOptions.user;
|
||||
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||
// string, then a byte array, re-encode it and wrap that in an array.
|
||||
const stringId = decodeURIComponent(window.atob(userId));
|
||||
user.id = u8arr(b64enc(u8arr(stringId)));
|
||||
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
||||
|
||||
return {
|
||||
...credentialCreateOptions,
|
||||
challenge,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
export interface Assertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
registrationClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
attestationObject: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the binary data in the credential into base64 strings
|
||||
* for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||
const attObj = new Uint8Array(
|
||||
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
response: {
|
||||
clientDataJSON: b64enc(clientDataJSON),
|
||||
attestationObject: b64enc(attObj),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformCredentialRequestOptions(
|
||||
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
const challenge = u8arr(credentialRequestOptions.challenge.toString());
|
||||
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||
(credentialDescriptor) => {
|
||||
const id = u8arr(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...credentialRequestOptions,
|
||||
challenge,
|
||||
allowCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthAssertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
assertionClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
authenticatorData: string;
|
||||
signature: string;
|
||||
userHandle: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
const sig = new Uint8Array(response.signature);
|
||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
|
||||
response: {
|
||||
clientDataJSON: b64RawEnc(clientDataJSON),
|
||||
signature: b64RawEnc(sig),
|
||||
authenticatorData: b64RawEnc(authData),
|
||||
userHandle: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { checkWebAuthnSupport } from "@goauthentik/common/helpers/webauthn";
|
||||
import {
|
||||
checkWebAuthnSupport,
|
||||
transformAssertionForServer,
|
||||
transformCredentialRequestOptions,
|
||||
} from "@goauthentik/common/helpers/webauthn";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base";
|
||||
|
||||
@ -34,12 +38,12 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||
async authenticate(): Promise<void> {
|
||||
// request the authenticator to create an assertion signature using the
|
||||
// credential private key
|
||||
let assertion: PublicKeyCredential;
|
||||
let assertion;
|
||||
checkWebAuthnSupport();
|
||||
try {
|
||||
assertion = (await navigator.credentials.get({
|
||||
assertion = await navigator.credentials.get({
|
||||
publicKey: this.transformedCredentialRequestOptions,
|
||||
})) as PublicKeyCredential;
|
||||
});
|
||||
if (!assertion) {
|
||||
throw new Error("Assertions is empty");
|
||||
}
|
||||
@ -47,11 +51,17 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||
throw new Error(`Error when creating credential: ${err}`);
|
||||
}
|
||||
|
||||
// we now have an authentication assertion! encode the byte arrays contained
|
||||
// in the assertion data as strings for posting to the server
|
||||
const transformedAssertionForServer = transformAssertionForServer(
|
||||
assertion as PublicKeyCredential,
|
||||
);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
try {
|
||||
await this.host?.submit(
|
||||
{
|
||||
webauthn: assertion.toJSON(),
|
||||
webauthn: transformedAssertionForServer,
|
||||
},
|
||||
{
|
||||
invisible: true,
|
||||
@ -64,10 +74,12 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
||||
// convert certain members of the PublicKeyCredentialRequestOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
const credentialRequestOptions = this.deviceChallenge
|
||||
?.challenge as unknown as PublicKeyCredentialRequestOptionsJSON;
|
||||
?.challenge as PublicKeyCredentialRequestOptions;
|
||||
this.transformedCredentialRequestOptions =
|
||||
PublicKeyCredential.parseRequestOptionsFromJSON(credentialRequestOptions);
|
||||
transformCredentialRequestOptions(credentialRequestOptions);
|
||||
this.authenticateWrapper();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { checkWebAuthnSupport } from "@goauthentik/common/helpers/webauthn";
|
||||
import {
|
||||
Assertion,
|
||||
checkWebAuthnSupport,
|
||||
transformCredentialCreateOptions,
|
||||
transformNewAssertionForServer,
|
||||
} from "@goauthentik/common/helpers/webauthn";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
@ -19,6 +24,10 @@ import {
|
||||
AuthenticatorWebAuthnChallengeResponseRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
|
||||
response: Assertion;
|
||||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-webauthn")
|
||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
@ -59,7 +68,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
}
|
||||
checkWebAuthnSupport();
|
||||
// request the authenticator(s) to create a new credential keypair.
|
||||
let credential: PublicKeyCredential;
|
||||
let credential;
|
||||
try {
|
||||
credential = (await navigator.credentials.create({
|
||||
publicKey: this.publicKeyCredentialCreateOptions,
|
||||
@ -71,12 +80,16 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
throw new Error(msg(str`Error creating credential: ${err}`));
|
||||
}
|
||||
|
||||
// we now have a new credential! We now need to encode the byte arrays
|
||||
// in the credential into strings, for posting to our server.
|
||||
const newAssertionForServer = transformNewAssertionForServer(credential);
|
||||
|
||||
// post the transformed credential data to the server for validation
|
||||
// and storing the public key
|
||||
try {
|
||||
await this.host?.submit(
|
||||
{
|
||||
response: credential.toJSON(),
|
||||
response: newAssertionForServer,
|
||||
},
|
||||
{
|
||||
invisible: true,
|
||||
@ -105,10 +118,12 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
||||
this.publicKeyCredentialCreateOptions =
|
||||
PublicKeyCredential.parseCreationOptionsFromJSON(
|
||||
this.challenge?.registration as PublicKeyCredentialCreationOptionsJSON,
|
||||
);
|
||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
||||
this.challenge?.registration.user.id,
|
||||
);
|
||||
this.registerWrapper();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import "construct-style-sheets-polyfill";
|
||||
import "@webcomponents/webcomponentsjs";
|
||||
import "lit/polyfill-support.js";
|
||||
import "core-js/actual";
|
||||
import "webauthn-polyfills";
|
||||
|
||||
import "@formatjs/intl-listformat/polyfill";
|
||||
import "@formatjs/intl-listformat/locale-data/en";
|
||||
|
@ -8,9 +8,7 @@ To prevent infinite loops (events created by policies which are attached to a No
|
||||
|
||||
## Filtering Events
|
||||
|
||||
Starting with authentik 0.15, you can create notification rules, which can alert you based on the creation of certain events.
|
||||
|
||||
Filtering is done by using the Policy Engine. You can do simple filtering using the "Event Matcher Policy" type.
|
||||
An authentik administrator can create notification rules based on the creation of specified events. Filtering is done by using the Policy Engine. You can do simple filtering using the "Event Matcher Policy" type.
|
||||
|
||||

|
||||
|
||||
|
@ -108,4 +108,4 @@ To confirm that authentik is properly configured with Actual Budget, visit your
|
||||
|
||||
## Resources
|
||||
|
||||
- [Official Actual Budget documentation on OpenID Connect integration](https://actualbudget.org/docs/experimental/oauth-auth/)
|
||||
- [Actual Budget docs - Authenticating With an OpenID Provider](https://actualbudget.org/docs/config/oauth-auth/)
|
||||
|
@ -1,75 +0,0 @@
|
||||
---
|
||||
title: Integrate with Planka
|
||||
sidebar_label: Planka
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is Planka
|
||||
|
||||
> Planka is an open-source, Trello-like application designed for project management using a Kanban board system.
|
||||
>
|
||||
> -- https://planka.app/
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `planka.company` is the FQDN of the Planka installation.
|
||||
|
||||
:::note
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of Planka with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **OAuth2/OpenID Connect** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Note the **Client ID** and **Client Secret** values because they will be required later.
|
||||
- Set a `Strict` redirect URI to `https://planka.company/oidc-callback`.
|
||||
- Select any available signing key.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create a group in authentik _(optional)_
|
||||
|
||||
To provision users in Planka with administrative permissions, you will need to create a group in authentik.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Directory** > **Groups** and click **Create**.
|
||||
3. Set a name for the group (e.g. `Planka Admins`) and click **Create**.
|
||||
4. Click the name of the newly created group, then switch to the **Users** tab.
|
||||
5. Click **Add existing user**, select the user who requires Planka administrator access, and click **Add**.
|
||||
|
||||
## Planka configuration
|
||||
|
||||
Add the following required environment variables to your Planka deployment:
|
||||
|
||||
```yaml
|
||||
OIDC_ISSUER=https://authentik.company/application/o/<application_slug>/
|
||||
OIDC_CLIENT_ID=<client if from authentik>
|
||||
OIDC_CLIENT_SECRET=<client secret from authentik>
|
||||
|
||||
#Optionally, if you want to provision users with administrator access, include the following environment variable:
|
||||
OIDC_ADMIN_ROLES=<authentik group name>
|
||||
|
||||
#Optionally, if you want to enforce the use of SSO and disable local authentication, include the following environment variable:
|
||||
OIDC_ENFORCED=true
|
||||
```
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To verify the integration with Planka, log out and attempt to log back in using the **Log in with SSO** button. You should be redirected to authentik. Once authenticated, you should then be redirected to the Planka dashboard.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Planka Docs - OIDC (OpenID Connect)](https://docs.planka.cloud/docs/configuration/oidc)
|
49
website/package-lock.json
generated
49
website/package-lock.json
generated
@ -19,6 +19,7 @@
|
||||
"@goauthentik/docusaurus-config": "^1.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@swc/html-linux-x64-gnu": "1.12.4",
|
||||
"clsx": "^2.1.1",
|
||||
"docusaurus-plugin-openapi-docs": "^4.4.0",
|
||||
"docusaurus-theme-openapi-docs": "^4.4.0",
|
||||
@ -64,12 +65,12 @@
|
||||
"@rspack/binding-darwin-arm64": "1.3.15",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.3.15",
|
||||
"@rspack/binding-linux-x64-gnu": "1.3.15",
|
||||
"@swc/core-darwin-arm64": "1.12.1",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.1",
|
||||
"@swc/core-linux-x64-gnu": "1.12.1",
|
||||
"@swc/html-darwin-arm64": "1.12.1",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.1",
|
||||
"@swc/html-linux-x64-gnu": "1.12.1",
|
||||
"@swc/core-darwin-arm64": "1.12.4",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.4",
|
||||
"@swc/core-linux-x64-gnu": "1.12.4",
|
||||
"@swc/html-darwin-arm64": "1.12.4",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.4",
|
||||
"@swc/html-linux-x64-gnu": "1.12.4",
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1"
|
||||
@ -5592,9 +5593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.1.tgz",
|
||||
"integrity": "sha512-nUjWVcJ3YS2N40ZbKwYO2RJ4+o2tWYRzNOcIQp05FqW0+aoUCVMdAUUzQinPDynfgwVshDAXCKemY8X7nN5MaA==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.4.tgz",
|
||||
"integrity": "sha512-HihKfeitjZU2ab94Zf893sxzFryLKX0TweGsNXXOLNtkSMLw50auuYfpRM0BOL9/uXXtuCWgRIF6P030SAX5xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5640,9 +5641,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.1.tgz",
|
||||
"integrity": "sha512-BxJDIJPq1+aCh9UsaSAN6wo3tuln8UhNXruOrzTI8/ElIig/3sAueDM6Eq7GvZSGGSA7ljhNATMJ0elD7lFatQ==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.4.tgz",
|
||||
"integrity": "sha512-n0IY76w+Scx8m3HIVRvLkoResuwsQgjDfAk9bxn99dq4leQO+mE0fkPl0Yw/1BIsPh+kxGfopIJH9zsZ1Z2YrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5672,9 +5673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.1.tgz",
|
||||
"integrity": "sha512-CrYnV8SZIgArQ9LKH0xEF95PKXzX9WkRSc5j55arOSBeDCeDUQk1Bg/iKdnDiuj5HC1hZpvzwMzSBJjv+Z70jA==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.4.tgz",
|
||||
"integrity": "sha512-6S50Xd/7ePjEwrXyHMxpKTZ+KBrgUwMA8hQPbArUOwH4S5vHBr51heL0iXbUkppn1bkSr0J0IbOove5hzn+iqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -5830,9 +5831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-darwin-arm64": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.1.tgz",
|
||||
"integrity": "sha512-vbCqYgBBdoxlsnUe/G6irBJ69LUOrlLVXgdxWxDSZ3YcbnpVmwi5YEeaRvqf4vNzZ/nzBMd4DYl6KK2Qsi0prw==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.4.tgz",
|
||||
"integrity": "sha512-mMBPb3mmS4eVekAq1cE5WM/fdO41Vuu3YJ96bIpF4VU68XAoRLcqkpf3rjp+EPWTQIEDQ3XwaqzF4CqPDYEP4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5878,9 +5879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-linux-arm64-gnu": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.1.tgz",
|
||||
"integrity": "sha512-KbqPLtsPVt0/kjp7sUT1APfEtNQUqMam3S0RzJkvuMz9jB2F9DREvj5EG+DPnx2s/kxnDm4sh9vM2sG2xNHErQ==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.4.tgz",
|
||||
"integrity": "sha512-wNNEHTjkgXwgkzlnEE+SpqAtHEXUqAzaBRY4XZyyM3q9ubj0X8m5/cFtIYQhpcwvNcygpTrUsbi+iNOVMyG6AA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5910,9 +5911,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-linux-x64-gnu": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.1.tgz",
|
||||
"integrity": "sha512-9QNCTgCZtyQVifLXqDTW7v4lgaC11v0/iL9OhsSZ19ycJrBmnxBmZtDIbuQrXAIzE1GD8mMOK/GLey2IeceoDQ==",
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.4.tgz",
|
||||
"integrity": "sha512-8t+bqm6ZAF18qg3GmlwJYIt8mX+OgAHA2hG9J4oaWD3Np+cTmbOIPVvjviAzHu1ne8OqKaekH8nTdSVKw2KK3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -78,12 +78,12 @@
|
||||
"@rspack/binding-darwin-arm64": "1.3.15",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.3.15",
|
||||
"@rspack/binding-linux-x64-gnu": "1.3.15",
|
||||
"@swc/core-darwin-arm64": "1.12.1",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.1",
|
||||
"@swc/core-linux-x64-gnu": "1.12.1",
|
||||
"@swc/html-darwin-arm64": "1.12.1",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.1",
|
||||
"@swc/html-linux-x64-gnu": "1.12.1",
|
||||
"@swc/core-darwin-arm64": "1.12.4",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.4",
|
||||
"@swc/core-linux-x64-gnu": "1.12.4",
|
||||
"@swc/html-darwin-arm64": "1.12.4",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.4",
|
||||
"@swc/html-linux-x64-gnu": "1.12.4",
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1"
|
||||
|
@ -24,7 +24,6 @@ const items = [
|
||||
"services/onlyoffice/index",
|
||||
"services/openproject/index",
|
||||
"services/owncloud/index",
|
||||
"services/planka/index",
|
||||
"services/rocketchat/index",
|
||||
"services/roundcube/index",
|
||||
"services/sharepoint-se/index",
|
||||
|
Reference in New Issue
Block a user