enterprise/providers/rac: connection token management (#8467)
This commit is contained in:
53
authentik/enterprise/providers/rac/api/connection_tokens.py
Normal file
53
authentik/enterprise/providers/rac/api/connection_tokens.py
Normal 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]
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
]
|
||||
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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": []
|
||||
|
@ -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
|
||||
|
315
schema.yml
315
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
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
98
web/src/admin/providers/rac/ConnectionTokenList.ts
Normal file
98
web/src/admin/providers/rac/ConnectionTokenList.ts
Normal 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}`];
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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")}"
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import { randomId } from "../utils/randomId";
|
||||
export interface RadioOption<T> {
|
||||
label: string;
|
||||
description?: TemplateResult;
|
||||
default: boolean;
|
||||
default?: boolean;
|
||||
value: T;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user