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, + }); + }} + > + + `; + } + + 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( + "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; }