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 <jens@goauthentik.io>

* unrelated: put outpost settings in group

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2024-01-04 16:27:16 +01:00 committed by GitHub
parent 519062bc39
commit 2064395434
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 29 deletions

View file

@ -60,6 +60,7 @@ class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
"property_mappings",
"auth_mode",
"launch_url",
"maximum_connections",
]

View file

@ -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),
),
]

View file

@ -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

View file

@ -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": "",

View file

@ -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,

View file

@ -8958,6 +8958,12 @@
"prompt"
],
"title": "Auth mode"
},
"maximum_connections": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Maximum connections"
}
},
"required": []

View file

@ -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

View file

@ -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,6 +221,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-group aria-label="Advanced settings">
<span slot="header"> ${msg("Advanced settings")} </span>
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
@ -238,6 +241,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
>${msg("Documentation")}</a
>
</p>
</ak-form-element-horizontal>`;
</ak-form-element-horizontal>
</ak-form-group>`;
}
}

View file

@ -106,6 +106,23 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
/>
<p class="pf-c-form__helper-text">${msg("Hostname/IP to connect to.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Maximum concurrent connections")}
name="maximumConnections"
?required=${true}
>
<input
type="number"
value="${first(this.instance?.maximumConnections, 1)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Maximum concurrent allowed connections to this endpoint. Can be set to -1 to disable the limit.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
?required=${true}