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:
parent
519062bc39
commit
2064395434
|
@ -60,6 +60,7 @@ class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
|||
"property_mappings",
|
||||
"auth_mode",
|
||||
"launch_url",
|
||||
"maximum_connections",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -8958,6 +8958,12 @@
|
|||
"prompt"
|
||||
],
|
||||
"title": "Auth mode"
|
||||
},
|
||||
"maximum_connections": {
|
||||
"type": "integer",
|
||||
"minimum": -2147483648,
|
||||
"maximum": 2147483647,
|
||||
"title": "Maximum connections"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
|
12
schema.yml
12
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
|
||||
|
|
|
@ -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<Outpost, string> {
|
|||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(
|
||||
this.instance ? this.instance.config : this.defaultConfig?.config,
|
||||
)}"
|
||||
></ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Set custom attributes using YAML or JSON.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("See more here:")}
|
||||
<a
|
||||
target="_blank"
|
||||
href="${docLink("/docs/outposts?utm_source=authentik#configuration")}"
|
||||
>${msg("Documentation")}</a
|
||||
>
|
||||
</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}
|
||||
value="${YAML.stringify(
|
||||
this.instance ? this.instance.config : this.defaultConfig?.config,
|
||||
)}"
|
||||
></ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Set custom attributes using YAML or JSON.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("See more here:")}
|
||||
<a
|
||||
target="_blank"
|
||||
href="${docLink("/docs/outposts?utm_source=authentik#configuration")}"
|
||||
>${msg("Documentation")}</a
|
||||
>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
Reference in a new issue