From 4733778460df334ef12af892660a153e7019bc9e Mon Sep 17 00:00:00 2001
From: Jens L
Date: Wed, 14 Feb 2024 18:57:11 +0100
Subject: [PATCH] enterprise/providers/rac: connection token management (#8467)
---
.../providers/rac/api/connection_tokens.py | 53 +++
.../enterprise/providers/rac/api/providers.py | 7 +-
.../providers/rac/consumer_client.py | 12 +-
..._alter_connectiontoken_options_and_more.py | 181 ++++++++++
..._alter_connectiontoken_options_and_more.py | 28 ++
authentik/enterprise/providers/rac/models.py | 14 +
authentik/enterprise/providers/rac/signals.py | 4 +-
.../providers/rac/tests/test_endpoints_api.py | 3 +
authentik/enterprise/providers/rac/urls.py | 2 +
...ve_systemtask_finish_timestamp_and_more.py | 3 -
authentik/events/models.py | 6 +-
blueprints/schema.json | 5 +
blueprints/system/providers-rac.yaml | 2 +
schema.yml | 315 ++++++++++++++++++
.../admin/enterprise/EnterpriseLicenseForm.ts | 22 +-
.../PropertyMappingRACForm.ts | 130 +++-----
.../providers/rac/ConnectionTokenList.ts | 98 ++++++
.../admin/providers/rac/RACProviderForm.ts | 22 ++
.../providers/rac/RACProviderViewPage.ts | 10 +
web/src/admin/users/UserViewPage.ts | 11 +
web/src/common/constants.ts | 1 +
web/src/elements/Interface/Interface.ts | 13 +-
web/src/elements/forms/Radio.ts | 2 +-
23 files changed, 846 insertions(+), 98 deletions(-)
create mode 100644 authentik/enterprise/providers/rac/api/connection_tokens.py
create mode 100644 authentik/enterprise/providers/rac/migrations/0001_squashed_0003_alter_connectiontoken_options_and_more.py
create mode 100644 authentik/enterprise/providers/rac/migrations/0003_alter_connectiontoken_options_and_more.py
create mode 100644 web/src/admin/providers/rac/ConnectionTokenList.ts
diff --git a/authentik/enterprise/providers/rac/api/connection_tokens.py b/authentik/enterprise/providers/rac/api/connection_tokens.py
new file mode 100644
index 0000000000..9b5ff10743
--- /dev/null
+++ b/authentik/enterprise/providers/rac/api/connection_tokens.py
@@ -0,0 +1,53 @@
+"""RAC Provider API Views"""
+
+from django_filters.rest_framework.backends import DjangoFilterBackend
+from rest_framework import mixins
+from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import GenericViewSet
+
+from authentik.api.authorization import OwnerFilter, OwnerPermissions
+from authentik.core.api.groups import GroupMemberSerializer
+from authentik.core.api.used_by import UsedByMixin
+from authentik.enterprise.api import EnterpriseRequiredMixin
+from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
+from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
+from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
+
+
+class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
+ """ConnectionToken Serializer"""
+
+ provider_obj = RACProviderSerializer(source="provider", read_only=True)
+ endpoint_obj = EndpointSerializer(source="endpoint", read_only=True)
+ user = GroupMemberSerializer(source="session.user", read_only=True)
+
+ class Meta:
+ model = Endpoint
+ fields = [
+ "pk",
+ "provider",
+ "provider_obj",
+ "endpoint",
+ "endpoint_obj",
+ "user",
+ ]
+
+
+class ConnectionTokenViewSet(
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ UsedByMixin,
+ mixins.ListModelMixin,
+ GenericViewSet,
+):
+ """ConnectionToken Viewset"""
+
+ queryset = ConnectionToken.objects.all().select_related("session", "endpoint")
+ serializer_class = ConnectionTokenSerializer
+ filterset_fields = ["endpoint", "session__user", "provider"]
+ search_fields = ["endpoint__name", "provider__name"]
+ ordering = ["endpoint__name", "provider__name"]
+ permission_classes = [OwnerPermissions]
+ filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
diff --git a/authentik/enterprise/providers/rac/api/providers.py b/authentik/enterprise/providers/rac/api/providers.py
index 25df75789c..892e081c96 100644
--- a/authentik/enterprise/providers/rac/api/providers.py
+++ b/authentik/enterprise/providers/rac/api/providers.py
@@ -16,7 +16,12 @@ class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
class Meta:
model = RACProvider
- fields = ProviderSerializer.Meta.fields + ["settings", "outpost_set", "connection_expiry"]
+ fields = ProviderSerializer.Meta.fields + [
+ "settings",
+ "outpost_set",
+ "connection_expiry",
+ "delete_token_on_disconnect",
+ ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
diff --git a/authentik/enterprise/providers/rac/consumer_client.py b/authentik/enterprise/providers/rac/consumer_client.py
index 5bfc176b95..b6331ca563 100644
--- a/authentik/enterprise/providers/rac/consumer_client.py
+++ b/authentik/enterprise/providers/rac/consumer_client.py
@@ -43,6 +43,7 @@ class RACClientConsumer(AsyncWebsocketConsumer):
logger: BoundLogger
async def connect(self):
+ self.logger = get_logger()
await self.accept("guacamole")
await self.channel_layer.group_add(RAC_CLIENT_GROUP, self.channel_name)
await self.channel_layer.group_add(
@@ -64,9 +65,11 @@ class RACClientConsumer(AsyncWebsocketConsumer):
@database_sync_to_async
def init_outpost_connection(self):
"""Initialize guac connection settings"""
- self.token = ConnectionToken.filter_not_expired(
- token=self.scope["url_route"]["kwargs"]["token"]
- ).first()
+ self.token = (
+ ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
+ .select_related("endpoint", "provider", "session", "session__user")
+ .first()
+ )
if not self.token:
raise DenyConnection()
self.provider = self.token.provider
@@ -107,6 +110,9 @@ class RACClientConsumer(AsyncWebsocketConsumer):
OUTPOST_GROUP_INSTANCE % {"outpost_pk": str(outpost.pk), "instance": states[0].uid},
msg,
)
+ if self.provider and self.provider.delete_token_on_disconnect:
+ self.logger.info("Deleting connection token to prevent reconnect", token=self.token)
+ self.token.delete()
async def receive(self, text_data=None, bytes_data=None):
"""Mirror data received from client to the dest_channel_id
diff --git a/authentik/enterprise/providers/rac/migrations/0001_squashed_0003_alter_connectiontoken_options_and_more.py b/authentik/enterprise/providers/rac/migrations/0001_squashed_0003_alter_connectiontoken_options_and_more.py
new file mode 100644
index 0000000000..3c6626f1a7
--- /dev/null
+++ b/authentik/enterprise/providers/rac/migrations/0001_squashed_0003_alter_connectiontoken_options_and_more.py
@@ -0,0 +1,181 @@
+# Generated by Django 5.0.1 on 2024-02-11 19:04
+
+import uuid
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import authentik.core.models
+import authentik.lib.utils.time
+
+
+class Migration(migrations.Migration):
+
+ replaces = [
+ ("authentik_providers_rac", "0001_initial"),
+ ("authentik_providers_rac", "0002_endpoint_maximum_connections"),
+ ("authentik_providers_rac", "0003_alter_connectiontoken_options_and_more"),
+ ]
+
+ initial = True
+
+ dependencies = [
+ ("authentik_core", "0032_group_roles"),
+ ("authentik_policies", "0011_policybinding_failure_result_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="RACPropertyMapping",
+ fields=[
+ (
+ "propertymapping_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="authentik_core.propertymapping",
+ ),
+ ),
+ ("static_settings", models.JSONField(default=dict)),
+ ],
+ options={
+ "verbose_name": "RAC Property Mapping",
+ "verbose_name_plural": "RAC Property Mappings",
+ },
+ bases=("authentik_core.propertymapping",),
+ ),
+ migrations.CreateModel(
+ name="RACProvider",
+ fields=[
+ (
+ "provider_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="authentik_core.provider",
+ ),
+ ),
+ ("settings", models.JSONField(default=dict)),
+ (
+ "auth_mode",
+ models.TextField(
+ choices=[("static", "Static"), ("prompt", "Prompt")], default="prompt"
+ ),
+ ),
+ (
+ "connection_expiry",
+ models.TextField(
+ default="hours=8",
+ help_text="Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)",
+ validators=[authentik.lib.utils.time.timedelta_string_validator],
+ ),
+ ),
+ (
+ "delete_token_on_disconnect",
+ models.BooleanField(
+ default=False,
+ help_text="When set to true, connection tokens will be deleted upon disconnect.",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "RAC Provider",
+ "verbose_name_plural": "RAC Providers",
+ },
+ bases=("authentik_core.provider",),
+ ),
+ migrations.CreateModel(
+ name="Endpoint",
+ fields=[
+ (
+ "policybindingmodel_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="authentik_policies.policybindingmodel",
+ ),
+ ),
+ ("name", models.TextField()),
+ ("host", models.TextField()),
+ (
+ "protocol",
+ models.TextField(choices=[("rdp", "Rdp"), ("vnc", "Vnc"), ("ssh", "Ssh")]),
+ ),
+ ("settings", models.JSONField(default=dict)),
+ (
+ "auth_mode",
+ models.TextField(choices=[("static", "Static"), ("prompt", "Prompt")]),
+ ),
+ (
+ "property_mappings",
+ models.ManyToManyField(
+ blank=True, default=None, to="authentik_core.propertymapping"
+ ),
+ ),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="authentik_providers_rac.racprovider",
+ ),
+ ),
+ ("maximum_connections", models.IntegerField(default=1)),
+ ],
+ options={
+ "verbose_name": "RAC Endpoint",
+ "verbose_name_plural": "RAC Endpoints",
+ },
+ bases=("authentik_policies.policybindingmodel", models.Model),
+ ),
+ migrations.CreateModel(
+ name="ConnectionToken",
+ fields=[
+ (
+ "expires",
+ models.DateTimeField(default=authentik.core.models.default_token_duration),
+ ),
+ ("expiring", models.BooleanField(default=True)),
+ (
+ "connection_token_uuid",
+ models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
+ ),
+ ("token", models.TextField(default=authentik.core.models.default_token_key)),
+ ("settings", models.JSONField(default=dict)),
+ (
+ "endpoint",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="authentik_providers_rac.endpoint",
+ ),
+ ),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="authentik_providers_rac.racprovider",
+ ),
+ ),
+ (
+ "session",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="authentik_core.authenticatedsession",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ "verbose_name": "RAC Connection token",
+ "verbose_name_plural": "RAC Connection tokens",
+ },
+ ),
+ ]
diff --git a/authentik/enterprise/providers/rac/migrations/0003_alter_connectiontoken_options_and_more.py b/authentik/enterprise/providers/rac/migrations/0003_alter_connectiontoken_options_and_more.py
new file mode 100644
index 0000000000..c333fedadd
--- /dev/null
+++ b/authentik/enterprise/providers/rac/migrations/0003_alter_connectiontoken_options_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.1 on 2024-02-11 19:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_providers_rac", "0002_endpoint_maximum_connections"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="connectiontoken",
+ options={
+ "verbose_name": "RAC Connection token",
+ "verbose_name_plural": "RAC Connection tokens",
+ },
+ ),
+ migrations.AddField(
+ model_name="racprovider",
+ name="delete_token_on_disconnect",
+ field=models.BooleanField(
+ default=False,
+ help_text="When set to true, connection tokens will be deleted upon disconnect.",
+ ),
+ ),
+ ]
diff --git a/authentik/enterprise/providers/rac/models.py b/authentik/enterprise/providers/rac/models.py
index d354617739..c5f866bfd6 100644
--- a/authentik/enterprise/providers/rac/models.py
+++ b/authentik/enterprise/providers/rac/models.py
@@ -52,6 +52,10 @@ class RACProvider(Provider):
"(Format: hours=-1;minutes=-2;seconds=-3)"
),
)
+ delete_token_on_disconnect = models.BooleanField(
+ default=False,
+ help_text=_("When set to true, connection tokens will be deleted upon disconnect."),
+ )
@property
def launch_url(self) -> Optional[str]:
@@ -195,3 +199,13 @@ class ConnectionToken(ExpiringModel):
continue
settings[key] = str(value)
return settings
+
+ def __str__(self):
+ return (
+ f"RAC Connection token {self.session.user} to "
+ f"{self.endpoint.provider.name}/{self.endpoint.name}"
+ )
+
+ class Meta:
+ verbose_name = _("RAC Connection token")
+ verbose_name_plural = _("RAC Connection tokens")
diff --git a/authentik/enterprise/providers/rac/signals.py b/authentik/enterprise/providers/rac/signals.py
index 20e967ddb3..28cece00ab 100644
--- a/authentik/enterprise/providers/rac/signals.py
+++ b/authentik/enterprise/providers/rac/signals.py
@@ -45,8 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
@receiver(post_save, sender=Endpoint)
-def post_save_application(sender: type[Model], instance, created: bool, **_):
- """Clear user's application cache upon application creation"""
+def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
+ """Clear user's endpoint cache upon endpoint creation"""
if not created: # pragma: no cover
return
diff --git a/authentik/enterprise/providers/rac/tests/test_endpoints_api.py b/authentik/enterprise/providers/rac/tests/test_endpoints_api.py
index 3000b345ce..1ad9b70daf 100644
--- a/authentik/enterprise/providers/rac/tests/test_endpoints_api.py
+++ b/authentik/enterprise/providers/rac/tests/test_endpoints_api.py
@@ -70,6 +70,7 @@ class TestEndpointsAPI(APITestCase):
"authorization_flow": None,
"property_mappings": [],
"connection_expiry": "hours=8",
+ "delete_token_on_disconnect": False,
"component": "ak-provider-rac-form",
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
@@ -124,6 +125,7 @@ class TestEndpointsAPI(APITestCase):
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
"connection_expiry": "hours=8",
+ "delete_token_on_disconnect": False,
"verbose_name": "RAC Provider",
"verbose_name_plural": "RAC Providers",
"meta_model_name": "authentik_providers_rac.racprovider",
@@ -152,6 +154,7 @@ class TestEndpointsAPI(APITestCase):
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
"connection_expiry": "hours=8",
+ "delete_token_on_disconnect": False,
"verbose_name": "RAC Provider",
"verbose_name_plural": "RAC Providers",
"meta_model_name": "authentik_providers_rac.racprovider",
diff --git a/authentik/enterprise/providers/rac/urls.py b/authentik/enterprise/providers/rac/urls.py
index b36eb998d5..8ee5e32089 100644
--- a/authentik/enterprise/providers/rac/urls.py
+++ b/authentik/enterprise/providers/rac/urls.py
@@ -6,6 +6,7 @@ from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from authentik.core.channels import TokenOutpostMiddleware
+from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
@@ -45,4 +46,5 @@ api_urlpatterns = [
("providers/rac", RACProviderViewSet),
("propertymappings/rac", RACPropertyMappingViewSet),
("rac/endpoints", EndpointViewSet),
+ ("rac/connection_tokens", ConnectionTokenViewSet),
]
diff --git a/authentik/events/migrations/0005_remove_systemtask_finish_timestamp_and_more.py b/authentik/events/migrations/0005_remove_systemtask_finish_timestamp_and_more.py
index b42fb252fc..8871965b7f 100644
--- a/authentik/events/migrations/0005_remove_systemtask_finish_timestamp_and_more.py
+++ b/authentik/events/migrations/0005_remove_systemtask_finish_timestamp_and_more.py
@@ -23,18 +23,15 @@ class Migration(migrations.Migration):
model_name="systemtask",
name="duration",
field=models.FloatField(default=0),
- preserve_default=False,
),
migrations.AddField(
model_name="systemtask",
name="finish_timestamp",
field=models.DateTimeField(default=django.utils.timezone.now),
- preserve_default=False,
),
migrations.AddField(
model_name="systemtask",
name="start_timestamp",
field=models.DateTimeField(default=django.utils.timezone.now),
- preserve_default=False,
),
]
diff --git a/authentik/events/models.py b/authentik/events/models.py
index 179d9edf29..3bb2ff1458 100644
--- a/authentik/events/models.py
+++ b/authentik/events/models.py
@@ -620,9 +620,9 @@ class SystemTask(SerializerModel, ExpiringModel):
name = models.TextField()
uid = models.TextField(null=True)
- start_timestamp = models.DateTimeField()
- finish_timestamp = models.DateTimeField()
- duration = models.FloatField()
+ start_timestamp = models.DateTimeField(default=now)
+ finish_timestamp = models.DateTimeField(default=now)
+ duration = models.FloatField(default=0)
status = models.TextField(choices=TaskStatus.choices)
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 6a745cebb1..bd3c1cd29f 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -7924,6 +7924,11 @@
"minLength": 1,
"title": "Connection expiry",
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
+ },
+ "delete_token_on_disconnect": {
+ "type": "boolean",
+ "title": "Delete token on disconnect",
+ "description": "When set to true, connection tokens will be deleted upon disconnect."
}
},
"required": []
diff --git a/blueprints/system/providers-rac.yaml b/blueprints/system/providers-rac.yaml
index 63a568673f..ef530cea20 100644
--- a/blueprints/system/providers-rac.yaml
+++ b/blueprints/system/providers-rac.yaml
@@ -23,6 +23,8 @@ entries:
enable-full-window-drag: "true"
enable-desktop-composition: "true"
enable-menu-animations: "true"
+ enable-wallpaper: "true"
+ enable-font-smoothing: "true"
- identifiers:
managed: goauthentik.io/providers/rac/ssh-default
model: authentik_providers_rac.racpropertymapping
diff --git a/schema.yml b/schema.yml
index 97214b0055..4c7a433d00 100644
--- a/schema.yml
+++ b/schema.yml
@@ -17818,6 +17818,252 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
+ /rac/connection_tokens/:
+ get:
+ operationId: rac_connection_tokens_list
+ description: ConnectionToken Viewset
+ parameters:
+ - in: query
+ name: endpoint
+ schema:
+ type: string
+ format: uuid
+ - name: ordering
+ required: false
+ in: query
+ description: Which field to use when ordering the results.
+ schema:
+ type: string
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: page_size
+ required: false
+ in: query
+ description: Number of results to return per page.
+ schema:
+ type: integer
+ - in: query
+ name: provider
+ schema:
+ type: integer
+ - name: search
+ required: false
+ in: query
+ description: A search term.
+ schema:
+ type: string
+ - in: query
+ name: session__user
+ schema:
+ type: integer
+ tags:
+ - rac
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaginatedConnectionTokenList'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ /rac/connection_tokens/{connection_token_uuid}/:
+ get:
+ operationId: rac_connection_tokens_retrieve
+ description: ConnectionToken Viewset
+ parameters:
+ - in: path
+ name: connection_token_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this connection token.
+ required: true
+ tags:
+ - rac
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionToken'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ put:
+ operationId: rac_connection_tokens_update
+ description: ConnectionToken Viewset
+ parameters:
+ - in: path
+ name: connection_token_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this connection token.
+ required: true
+ tags:
+ - rac
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionTokenRequest'
+ required: true
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionToken'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ patch:
+ operationId: rac_connection_tokens_partial_update
+ description: ConnectionToken Viewset
+ parameters:
+ - in: path
+ name: connection_token_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this connection token.
+ required: true
+ tags:
+ - rac
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PatchedConnectionTokenRequest'
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionToken'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ delete:
+ operationId: rac_connection_tokens_destroy
+ description: ConnectionToken Viewset
+ parameters:
+ - in: path
+ name: connection_token_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this connection token.
+ required: true
+ tags:
+ - rac
+ security:
+ - authentik: []
+ responses:
+ '204':
+ description: No response body
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ /rac/connection_tokens/{connection_token_uuid}/used_by/:
+ get:
+ operationId: rac_connection_tokens_used_by_list
+ description: Get a list of all objects that use this object
+ parameters:
+ - in: path
+ name: connection_token_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this connection token.
+ required: true
+ tags:
+ - rac
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/UsedBy'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
/rac/endpoints/:
get:
operationId: rac_endpoints_list
@@ -31248,6 +31494,48 @@ components:
- cache_timeout_reputation
- capabilities
- error_reporting
+ ConnectionToken:
+ type: object
+ description: ConnectionToken Serializer
+ properties:
+ pk:
+ type: string
+ format: uuid
+ readOnly: true
+ title: Pbm uuid
+ provider:
+ type: integer
+ provider_obj:
+ allOf:
+ - $ref: '#/components/schemas/RACProvider'
+ readOnly: true
+ endpoint:
+ type: string
+ format: uuid
+ readOnly: true
+ endpoint_obj:
+ allOf:
+ - $ref: '#/components/schemas/Endpoint'
+ readOnly: true
+ user:
+ allOf:
+ - $ref: '#/components/schemas/GroupMember'
+ readOnly: true
+ required:
+ - endpoint
+ - endpoint_obj
+ - pk
+ - provider
+ - provider_obj
+ - user
+ ConnectionTokenRequest:
+ type: object
+ description: ConnectionToken Serializer
+ properties:
+ provider:
+ type: integer
+ required:
+ - provider
ConsentChallenge:
type: object
description: Challenge info for consent screens
@@ -36268,6 +36556,18 @@ components:
required:
- pagination
- results
+ PaginatedConnectionTokenList:
+ type: object
+ properties:
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/ConnectionToken'
+ required:
+ - pagination
+ - results
PaginatedConsentStageList:
type: object
properties:
@@ -37980,6 +38280,12 @@ components:
writeOnly: true
description: Optional Private Key. If this is set, you can use this keypair
for encryption.
+ PatchedConnectionTokenRequest:
+ type: object
+ description: ConnectionToken Serializer
+ properties:
+ provider:
+ type: integer
PatchedConsentStageRequest:
type: object
description: ConsentStage Serializer
@@ -39542,6 +39848,9 @@ components:
minLength: 1
description: 'Determines how long a session lasts. Default of 0 means that
the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)'
+ delete_token_on_disconnect:
+ type: boolean
+ description: When set to true, connection tokens will be deleted upon disconnect.
PatchedRadiusProviderRequest:
type: object
description: RadiusProvider Serializer
@@ -41615,6 +41924,9 @@ components:
type: string
description: 'Determines how long a session lasts. Default of 0 means that
the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)'
+ delete_token_on_disconnect:
+ type: boolean
+ description: When set to true, connection tokens will be deleted upon disconnect.
required:
- assigned_application_name
- assigned_application_slug
@@ -41656,6 +41968,9 @@ components:
minLength: 1
description: 'Determines how long a session lasts. Default of 0 means that
the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)'
+ delete_token_on_disconnect:
+ type: boolean
+ description: When set to true, connection tokens will be deleted upon disconnect.
required:
- authorization_flow
- name
diff --git a/web/src/admin/enterprise/EnterpriseLicenseForm.ts b/web/src/admin/enterprise/EnterpriseLicenseForm.ts
index ebce9938db..466a96aa71 100644
--- a/web/src/admin/enterprise/EnterpriseLicenseForm.ts
+++ b/web/src/admin/enterprise/EnterpriseLicenseForm.ts
@@ -1,3 +1,4 @@
+import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/app/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@@ -34,14 +35,19 @@ export class EnterpriseLicenseForm extends ModelForm {
}
async send(data: License): Promise {
- return this.instance
- ? new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({
- licenseUuid: this.instance.licenseUuid || "",
- patchedLicenseRequest: data,
- })
- : new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({
- licenseRequest: data,
- });
+ return (
+ this.instance
+ ? new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({
+ licenseUuid: this.instance.licenseUuid || "",
+ patchedLicenseRequest: data,
+ })
+ : new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({
+ licenseRequest: data,
+ })
+ ).then((data) => {
+ window.dispatchEvent(new CustomEvent(EVENT_REFRESH_ENTERPRISE));
+ return data;
+ });
}
renderForm(): TemplateResult {
diff --git a/web/src/admin/property-mappings/PropertyMappingRACForm.ts b/web/src/admin/property-mappings/PropertyMappingRACForm.ts
index 72e2bb0906..145a63556b 100644
--- a/web/src/admin/property-mappings/PropertyMappingRACForm.ts
+++ b/web/src/admin/property-mappings/PropertyMappingRACForm.ts
@@ -1,4 +1,3 @@
-import { first } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import "@goauthentik/elements/CodeMirror";
@@ -6,6 +5,8 @@ import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
+import "@goauthentik/elements/forms/Radio";
+import type { RadioOption } from "@goauthentik/elements/forms/Radio";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
@@ -14,6 +15,23 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { PropertymappingsApi, RACPropertyMapping } from "@goauthentik/api";
+export const staticSettingOptions: RadioOption[] = [
+ {
+ label: msg("Unconfigured"),
+ value: undefined,
+ default: true,
+ description: html`${msg("This option will not be changed by this mapping.")}`,
+ },
+ {
+ label: msg("Enabled"),
+ value: "true",
+ },
+ {
+ label: msg("Disabled"),
+ value: "false",
+ },
+];
+
@customElement("ak-property-mapping-rac-form")
export class PropertyMappingLDAPForm extends ModelForm {
loadInstance(pk: string): Promise {
@@ -58,7 +76,6 @@ export class PropertyMappingLDAPForm extends ModelForm
${msg("RDP settings")}
diff --git a/web/src/admin/providers/rac/ConnectionTokenList.ts b/web/src/admin/providers/rac/ConnectionTokenList.ts
new file mode 100644
index 0000000000..93ee5ddf15
--- /dev/null
+++ b/web/src/admin/providers/rac/ConnectionTokenList.ts
@@ -0,0 +1,98 @@
+import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { uiConfig } from "@goauthentik/common/ui/config";
+import "@goauthentik/elements/buttons/SpinnerButton";
+import "@goauthentik/elements/forms/DeleteBulkForm";
+import "@goauthentik/elements/forms/ModalForm";
+import { PaginatedResponse, Table } from "@goauthentik/elements/table/Table";
+import { TableColumn } from "@goauthentik/elements/table/Table";
+import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
+
+import { msg } from "@lit/localize";
+import { CSSResult, TemplateResult, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
+
+import { ConnectionToken, Endpoint, RACProvider, RacApi } from "@goauthentik/api";
+
+@customElement("ak-rac-connection-token-list")
+export class ConnectionTokenListPage extends Table {
+ checkbox = true;
+ clearOnRefresh = true;
+
+ searchEnabled(): boolean {
+ return true;
+ }
+
+ @property()
+ order = "name";
+
+ @property({ attribute: false })
+ provider?: RACProvider;
+
+ @property({ type: Number })
+ userId?: number;
+
+ static get styles(): CSSResult[] {
+ return super.styles.concat(PFDescriptionList);
+ }
+
+ async apiEndpoint(page: number): Promise> {
+ return new RacApi(DEFAULT_CONFIG).racConnectionTokensList({
+ ordering: this.order,
+ page: page,
+ pageSize: (await uiConfig()).pagination.perPage,
+ search: this.search || "",
+ provider: this.provider?.pk,
+ sessionUser: this.userId,
+ });
+ }
+
+ renderToolbarSelected(): TemplateResult {
+ const disabled = this.selectedElements.length < 1;
+ return html` {
+ return [
+ { key: msg("Name"), value: item.name },
+ { key: msg("Host"), value: item.host },
+ ];
+ }}
+ .usedBy=${(item: Endpoint) => {
+ return new RacApi(DEFAULT_CONFIG).racConnectionTokensUsedByList({
+ connectionTokenUuid: item.pk,
+ });
+ }}
+ .delete=${(item: Endpoint) => {
+ return new RacApi(DEFAULT_CONFIG).racConnectionTokensDestroy({
+ connectionTokenUuid: item.pk,
+ });
+ }}
+ >
+
+ ${msg("Delete")}
+
+ `;
+ }
+
+ columns(): TableColumn[] {
+ if (this.provider) {
+ return [
+ new TableColumn(msg("Endpoint"), "endpoint__name"),
+ new TableColumn(msg("User"), "session__user"),
+ ];
+ }
+ return [
+ new TableColumn(msg("Provider"), "provider__name"),
+ new TableColumn(msg("Endpoint"), "endpoint__name"),
+ ];
+ }
+
+ row(item: ConnectionToken): TemplateResult[] {
+ if (this.provider) {
+ return [html`${item.endpointObj.name}`, html`${item.user.username}`];
+ }
+ return [html`${item.providerObj.name}`, html`${item.endpointObj.name}`];
+ }
+}
diff --git a/web/src/admin/providers/rac/RACProviderForm.ts b/web/src/admin/providers/rac/RACProviderForm.ts
index b4b122cba4..46eae9cdc9 100644
--- a/web/src/admin/providers/rac/RACProviderForm.ts
+++ b/web/src/admin/providers/rac/RACProviderForm.ts
@@ -107,6 +107,28 @@ export class RACProviderFormPage extends ModelForm {
+
+
+
+
+
+
+
+
+ ${msg("Delete authorization on disconnect")}
+
+
+ ${msg(
+ "When enabled, connection authorizations will be deleted when a client disconnects. This will force clients with flaky internet connections to re-authorize the endpoint.",
+ )}
+
+
${msg("Protocol settings")}
diff --git a/web/src/admin/providers/rac/RACProviderViewPage.ts b/web/src/admin/providers/rac/RACProviderViewPage.ts
index 393fa43755..9b9665d689 100644
--- a/web/src/admin/providers/rac/RACProviderViewPage.ts
+++ b/web/src/admin/providers/rac/RACProviderViewPage.ts
@@ -1,4 +1,5 @@
import "@goauthentik/admin/providers/RelatedApplicationButton";
+import "@goauthentik/admin/providers/rac/ConnectionTokenList";
import "@goauthentik/admin/providers/rac/EndpointForm";
import "@goauthentik/admin/providers/rac/EndpointList";
import "@goauthentik/admin/providers/rac/RACProviderForm";
@@ -86,6 +87,15 @@ export class RACProviderViewPage extends AKElement {
${this.renderTabOverview()}
+
+
`;
}
diff --git a/web/src/common/constants.ts b/web/src/common/constants.ts
index fe40f5a768..ad77f5a3d8 100644
--- a/web/src/common/constants.ts
+++ b/web/src/common/constants.ts
@@ -19,6 +19,7 @@ export const EVENT_LOCALE_REQUEST = "ak-locale-request";
export const EVENT_REQUEST_POST = "ak-request-post";
export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change";
+export const EVENT_REFRESH_ENTERPRISE = "ak-refresh-enterprise";
export const WS_MSG_TYPE_MESSAGE = "message";
export const WS_MSG_TYPE_REFRESH = "refresh";
diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts
index b97b87168a..ce47189bc7 100644
--- a/web/src/elements/Interface/Interface.ts
+++ b/web/src/elements/Interface/Interface.ts
@@ -1,3 +1,4 @@
+import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/app/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { brand, config } from "@goauthentik/common/api/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
@@ -109,8 +110,16 @@ export class EnterpriseAwareInterface extends Interface {
constructor() {
super();
- new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => {
- this.licenseSummary = enterprise;
+ const refreshStatus = () => {
+ new EnterpriseApi(DEFAULT_CONFIG)
+ .enterpriseLicenseSummaryRetrieve()
+ .then((enterprise) => {
+ this.licenseSummary = enterprise;
+ });
+ };
+ refreshStatus();
+ window.addEventListener(EVENT_REFRESH_ENTERPRISE, () => {
+ refreshStatus();
});
}
}
diff --git a/web/src/elements/forms/Radio.ts b/web/src/elements/forms/Radio.ts
index 27fbdf0b91..87e09b6f37 100644
--- a/web/src/elements/forms/Radio.ts
+++ b/web/src/elements/forms/Radio.ts
@@ -14,7 +14,7 @@ import { randomId } from "../utils/randomId";
export interface RadioOption {
label: string;
description?: TemplateResult;
- default: boolean;
+ default?: boolean;
value: T;
}