From 20643954348fbda587b235c74d7bdbaa2bf25e28 Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 4 Jan 2024 16:27:16 +0100 Subject: [PATCH] enterprise/providers/rac: add option to limit concurrent connections to endpoint (#8053) * enterprise/providers/rac: add option to limit concurrent connections to endpoint Signed-off-by: Jens Langhammer * unrelated: put outpost settings in group Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .../enterprise/providers/rac/api/endpoints.py | 1 + .../0002_endpoint_maximum_connections.py | 17 ++++++++ authentik/enterprise/providers/rac/models.py | 1 + .../providers/rac/tests/test_endpoints_api.py | 3 ++ authentik/enterprise/providers/rac/views.py | 36 +++++++++++----- blueprints/schema.json | 6 +++ schema.yml | 12 ++++++ web/src/admin/outposts/OutpostForm.ts | 42 ++++++++++--------- web/src/admin/providers/rac/EndpointForm.ts | 17 ++++++++ 9 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 authentik/enterprise/providers/rac/migrations/0002_endpoint_maximum_connections.py diff --git a/authentik/enterprise/providers/rac/api/endpoints.py b/authentik/enterprise/providers/rac/api/endpoints.py index e1c6c5dd8..1af281ef8 100644 --- a/authentik/enterprise/providers/rac/api/endpoints.py +++ b/authentik/enterprise/providers/rac/api/endpoints.py @@ -60,6 +60,7 @@ class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer): "property_mappings", "auth_mode", "launch_url", + "maximum_connections", ] diff --git a/authentik/enterprise/providers/rac/migrations/0002_endpoint_maximum_connections.py b/authentik/enterprise/providers/rac/migrations/0002_endpoint_maximum_connections.py new file mode 100644 index 000000000..0760f2313 --- /dev/null +++ b/authentik/enterprise/providers/rac/migrations/0002_endpoint_maximum_connections.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0 on 2024-01-03 23:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_providers_rac", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="endpoint", + name="maximum_connections", + field=models.IntegerField(default=1), + ), + ] diff --git a/authentik/enterprise/providers/rac/models.py b/authentik/enterprise/providers/rac/models.py index f2806f32b..927bd23fe 100644 --- a/authentik/enterprise/providers/rac/models.py +++ b/authentik/enterprise/providers/rac/models.py @@ -81,6 +81,7 @@ class Endpoint(SerializerModel, PolicyBindingModel): settings = models.JSONField(default=dict) auth_mode = models.TextField(choices=AuthenticationMode.choices) provider = models.ForeignKey("RACProvider", on_delete=models.CASCADE) + maximum_connections = models.IntegerField(default=1) property_mappings = models.ManyToManyField( "authentik_core.PropertyMapping", default=None, blank=True diff --git a/authentik/enterprise/providers/rac/tests/test_endpoints_api.py b/authentik/enterprise/providers/rac/tests/test_endpoints_api.py index 0a659bccd..3000b345c 100644 --- a/authentik/enterprise/providers/rac/tests/test_endpoints_api.py +++ b/authentik/enterprise/providers/rac/tests/test_endpoints_api.py @@ -81,6 +81,7 @@ class TestEndpointsAPI(APITestCase): }, "protocol": "rdp", "host": self.allowed.host, + "maximum_connections": 1, "settings": {}, "property_mappings": [], "auth_mode": "", @@ -131,6 +132,7 @@ class TestEndpointsAPI(APITestCase): }, "protocol": "rdp", "host": self.allowed.host, + "maximum_connections": 1, "settings": {}, "property_mappings": [], "auth_mode": "", @@ -158,6 +160,7 @@ class TestEndpointsAPI(APITestCase): }, "protocol": "rdp", "host": self.denied.host, + "maximum_connections": 1, "settings": {}, "property_mappings": [], "auth_mode": "", diff --git a/authentik/enterprise/providers/rac/views.py b/authentik/enterprise/providers/rac/views.py index e50f6ee5b..4b93aee76 100644 --- a/authentik/enterprise/providers/rac/views.py +++ b/authentik/enterprise/providers/rac/views.py @@ -5,6 +5,7 @@ from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.timezone import now +from django.utils.translation import gettext as _ from authentik.core.models import Application, AuthenticatedSession from authentik.core.views.interface import InterfaceView @@ -79,35 +80,50 @@ class RACInterface(InterfaceView): class RACFinalStage(RedirectStage): """RAC Connection final stage, set the connection token in the stage""" + endpoint: Endpoint + provider: RACProvider + application: Application + def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: - endpoint: Endpoint = self.executor.current_stage.endpoint - engine = PolicyEngine(endpoint, self.request.user, self.request) + self.endpoint = self.executor.current_stage.endpoint + self.provider = self.executor.current_stage.provider + self.application = self.executor.current_stage.application + # Check policies bound to endpoint directly + engine = PolicyEngine(self.endpoint, self.request.user, self.request) engine.use_cache = False engine.build() passing = engine.result if not passing.passing: return self.executor.stage_invalid(", ".join(passing.messages)) + # Check if we're already at the maximum connection limit + all_tokens = ConnectionToken.filter_not_expired( + endpoint=self.endpoint, + ).exclude(endpoint__maximum_connections__lte=-1) + if all_tokens.count() >= self.endpoint.maximum_connections: + msg = [_("Maximum connection limit reached.")] + # Check if any other tokens exist for the current user, and inform them + # they are already connected + if all_tokens.filter(session__user=self.request.user).exists(): + msg.append(_("(You are already connected in another tab/window)")) + return self.executor.stage_invalid(" ".join(msg)) return super().dispatch(request, *args, **kwargs) def get_challenge(self, *args, **kwargs) -> RedirectChallenge: - endpoint: Endpoint = self.executor.current_stage.endpoint - provider: RACProvider = self.executor.current_stage.provider - application: Application = self.executor.current_stage.application token = ConnectionToken.objects.create( - provider=provider, - endpoint=endpoint, + provider=self.provider, + endpoint=self.endpoint, settings=self.executor.plan.context.get("connection_settings", {}), session=AuthenticatedSession.objects.filter( session_key=self.request.session.session_key ).first(), - expires=now() + timedelta_from_string(provider.connection_expiry), + expires=now() + timedelta_from_string(self.provider.connection_expiry), expiring=True, ) Event.new( EventAction.AUTHORIZE_APPLICATION, - authorized_application=application, + authorized_application=self.application, flow=self.executor.plan.flow_pk, - endpoint=endpoint.name, + endpoint=self.endpoint.name, ).from_http(self.request) setattr( self.executor.current_stage, diff --git a/blueprints/schema.json b/blueprints/schema.json index 07d9cd227..213cb1673 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -8958,6 +8958,12 @@ "prompt" ], "title": "Auth mode" + }, + "maximum_connections": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "title": "Maximum connections" } }, "required": [] diff --git a/schema.yml b/schema.yml index 09b351707..ed6a46fed 100644 --- a/schema.yml +++ b/schema.yml @@ -31381,6 +31381,10 @@ components: Build actual launch URL (the provider itself does not have one, just individual endpoints) readOnly: true + maximum_connections: + type: integer + maximum: 2147483647 + minimum: -2147483648 required: - auth_mode - host @@ -31412,6 +31416,10 @@ components: format: uuid auth_mode: $ref: '#/components/schemas/AuthModeEnum' + maximum_connections: + type: integer + maximum: 2147483647 + minimum: -2147483648 required: - auth_mode - host @@ -37298,6 +37306,10 @@ components: format: uuid auth_mode: $ref: '#/components/schemas/AuthModeEnum' + maximum_connections: + type: integer + maximum: 2147483647 + minimum: -2147483648 PatchedEventMatcherPolicyRequest: type: object description: Event Matcher Policy Serializer diff --git a/web/src/admin/outposts/OutpostForm.ts b/web/src/admin/outposts/OutpostForm.ts index 2c5ac9722..7c6c9dda5 100644 --- a/web/src/admin/outposts/OutpostForm.ts +++ b/web/src/admin/outposts/OutpostForm.ts @@ -3,6 +3,7 @@ import { docLink } from "@goauthentik/common/global"; import { groupBy } from "@goauthentik/common/utils"; import "@goauthentik/elements/CodeMirror"; 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/SearchSelect"; @@ -220,24 +221,27 @@ export class OutpostForm extends ModelForm { ${msg("Hold control/command to select multiple items.")}

- - -

- ${msg("Set custom attributes using YAML or JSON.")} -

-

- ${msg("See more here:")}  - ${msg("Documentation")} -

-
`; + + ${msg("Advanced settings")} + + +

+ ${msg("Set custom attributes using YAML or JSON.")} +

+

+ ${msg("See more here:")}  + ${msg("Documentation")} +

+
+
`; } } diff --git a/web/src/admin/providers/rac/EndpointForm.ts b/web/src/admin/providers/rac/EndpointForm.ts index af83af23f..0f23f4fca 100644 --- a/web/src/admin/providers/rac/EndpointForm.ts +++ b/web/src/admin/providers/rac/EndpointForm.ts @@ -106,6 +106,23 @@ export class EndpointForm extends ModelForm { />

${msg("Hostname/IP to connect to.")}

+ + +

+ ${msg( + "Maximum concurrent allowed connections to this endpoint. Can be set to -1 to disable the limit.", + )} +

+