enterprise/providers/rac: connection token management (#8467)

This commit is contained in:
Jens L
2024-02-14 18:57:11 +01:00
committed by GitHub
parent c048f4a356
commit 4733778460
23 changed files with 846 additions and 98 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": []

View File

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

View File

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

View File

@ -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<License, string> {
}
async send(data: License): Promise<License> {
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 {

View File

@ -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<string | undefined>[] = [
{
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<RACPropertyMapping, string> {
loadInstance(pk: string): Promise<RACPropertyMapping> {
@ -58,7 +76,6 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Username")}
?required=${true}
name="staticSettings.username"
>
<input
@ -70,7 +87,6 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Password")}
?required=${true}
name="staticSettings.password"
>
<input
@ -85,81 +101,45 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
<ak-form-group>
<span slot="header"> ${msg("RDP settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="staticSettings.ignore-cert">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(
this.instance?.staticSettings["ignore-cert"],
false,
)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Ignore server certificate")}</span
>
</label>
<ak-form-element-horizontal
label=${msg("Ignore server certificate")}
name="staticSettings.ignore-cert"
>
<ak-radio
.options=${staticSettingOptions}
.value=${this.instance?.staticSettings["ignore-cert"]}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="staticSettings.enable-wallpaper">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(
this.instance?.staticSettings["enable-wallpaper"],
false,
)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Enable wallpaper")}</span>
</label>
<ak-form-element-horizontal
label=${msg("Enable wallpaper")}
name="staticSettings.enable-wallpaper"
>
<ak-radio
.options=${staticSettingOptions}
.value=${this.instance?.staticSettings["enable-wallpaper"]}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="staticSettings.enable-font-smoothing">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(
this.instance?.staticSettings["enable-font-smoothing"],
false,
)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Enable font-smoothing")}</span>
</label>
<ak-form-element-horizontal
label=${msg("Enable font-smoothing")}
name="staticSettings.enable-font-smoothing"
>
<ak-radio
.options=${staticSettingOptions}
.value=${this.instance?.staticSettings["enable-font-smoothing"]}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="staticSettings.enable-full-window-drag">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(
this.instance?.staticSettings["enable-full-window-drag"],
false,
)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Enable full window dragging")}</span
>
</label>
<ak-form-element-horizontal
label=${msg("Enable full window dragging")}
name="staticSettings.enable-full-window-drag"
>
<ak-radio
.options=${staticSettingOptions}
.value=${this.instance?.staticSettings["enable-full-window-drag"]}
>
</ak-radio>
</ak-form-element-horizontal>
</div>
</ak-form-group>

View File

@ -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<ConnectionToken> {
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<PaginatedResponse<ConnectionToken>> {
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`<ak-forms-delete-bulk
objectLabel=${msg("Connection Token(s)")}
.objects=${this.selectedElements}
.metadata=${(item: Endpoint) => {
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,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
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}`];
}
}

View File

@ -107,6 +107,28 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="deleteTokenOnDisconnect">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.deleteTokenOnDisconnect, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Delete authorization on disconnect")}</span
>
</label>
<p class="pf-c-form__helper-text">
${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.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>

View File

@ -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 {
<section slot="page-overview" data-tab-title="${msg("Overview")}">
${this.renderTabOverview()}
</section>
<section slot="page-connections" data-tab-title="${msg("Connections")}">
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-rac-connection-token-list
.provider=${this.provider}
></ak-rac-connection-token-list>
</div>
</div>
</section>
<section
slot="page-changelog"
data-tab-title="${msg("Changelog")}"

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/groups/RelatedGroupList";
import "@goauthentik/admin/providers/rac/ConnectionTokenList";
import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserApplicationTable";
import "@goauthentik/admin/users/UserChart";
@ -329,6 +330,16 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</ak-user-settings-source>
</div>
</section>
<section
slot="page-rac-connection-tokens"
data-tab-title="${msg("RAC Connections")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<ak-rac-connection-token-list userId=${user.pk}>
</ak-rac-connection-token-list>
</div>
</section>
</ak-tabs>
`;
}

View File

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

View File

@ -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();
});
}
}

View File

@ -14,7 +14,7 @@ import { randomId } from "../utils/randomId";
export interface RadioOption<T> {
label: string;
description?: TemplateResult;
default: boolean;
default?: boolean;
value: T;
}