diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py
index 1e7436be9..d0fa7267b 100644
--- a/authentik/core/api/propertymappings.py
+++ b/authentik/core/api/propertymappings.py
@@ -19,6 +19,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.models import PropertyMapping
+from authentik.enterprise.apps import EnterpriseConfig
from authentik.events.utils import sanitize_item
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestSerializer
@@ -95,6 +96,7 @@ class PropertyMappingViewSet(
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
+ "requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
}
)
return Response(TypeCreateSerializer(data, many=True).data)
diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py
index a5095dcde..6c0f4db06 100644
--- a/authentik/core/api/providers.py
+++ b/authentik/core/api/providers.py
@@ -16,6 +16,7 @@ from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Provider
+from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import all_subclasses
@@ -113,6 +114,7 @@ class ProviderViewSet(
"description": subclass.__doc__,
"component": subclass().component,
"model_name": subclass._meta.model_name,
+ "requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
}
)
data.append(
diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py
index c7a188f5c..c79fec22e 100644
--- a/authentik/core/api/utils.py
+++ b/authentik/core/api/utils.py
@@ -5,7 +5,7 @@ from django.db.models import Model
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
from drf_spectacular.plumbing import build_basic_type
from drf_spectacular.types import OpenApiTypes
-from rest_framework.fields import CharField, IntegerField, JSONField
+from rest_framework.fields import BooleanField, CharField, IntegerField, JSONField
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
@@ -74,6 +74,7 @@ class TypeCreateSerializer(PassiveSerializer):
description = CharField(required=True)
component = CharField(required=True)
model_name = CharField(required=True)
+ requires_enterprise = BooleanField(default=False)
class CacheSerializer(PassiveSerializer):
diff --git a/authentik/enterprise/api.py b/authentik/enterprise/api.py
index fdf0a11fc..c13e34b6d 100644
--- a/authentik/enterprise/api.py
+++ b/authentik/enterprise/api.py
@@ -2,9 +2,11 @@
from datetime import datetime, timedelta
from django.utils.timezone import now
+from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, DateTimeField, IntegerField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
@@ -20,6 +22,18 @@ from authentik.enterprise.models import License, LicenseKey
from authentik.root.install_id import get_install_id
+class EnterpriseRequiredMixin:
+ """Mixin to validate that a valid enterprise license
+ exists before allowing to safe the object"""
+
+ def validate(self, attrs: dict) -> dict:
+ """Check that a valid license exists"""
+ total = LicenseKey.get_total()
+ if not total.is_valid():
+ raise ValidationError(_("Enterprise is required to create/update this object."))
+ return super().validate(attrs)
+
+
class LicenseSerializer(ModelSerializer):
"""License Serializer"""
diff --git a/authentik/enterprise/apps.py b/authentik/enterprise/apps.py
index 2d918da17..a0b9bed6d 100644
--- a/authentik/enterprise/apps.py
+++ b/authentik/enterprise/apps.py
@@ -2,7 +2,11 @@
from authentik.blueprints.apps import ManagedAppConfig
-class AuthentikEnterpriseConfig(ManagedAppConfig):
+class EnterpriseConfig(ManagedAppConfig):
+ """Base app config for all enterprise apps"""
+
+
+class AuthentikEnterpriseConfig(EnterpriseConfig):
"""Enterprise app config"""
name = "authentik.enterprise"
diff --git a/authentik/enterprise/providers/rac/api/endpoints.py b/authentik/enterprise/providers/rac/api/endpoints.py
index b0b0239c5..e1c6c5dd8 100644
--- a/authentik/enterprise/providers/rac/api/endpoints.py
+++ b/authentik/enterprise/providers/rac/api/endpoints.py
@@ -15,6 +15,7 @@ from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Provider
+from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import Endpoint
from authentik.policies.engine import PolicyEngine
@@ -28,7 +29,7 @@ def user_endpoint_cache_key(user_pk: str) -> str:
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
-class EndpointSerializer(ModelSerializer):
+class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
"""Endpoint Serializer"""
provider_obj = RACProviderSerializer(source="provider", read_only=True)
diff --git a/authentik/enterprise/providers/rac/api/property_mappings.py b/authentik/enterprise/providers/rac/api/property_mappings.py
index 35daec95c..4afef68bb 100644
--- a/authentik/enterprise/providers/rac/api/property_mappings.py
+++ b/authentik/enterprise/providers/rac/api/property_mappings.py
@@ -5,10 +5,11 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField
+from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.models import RACPropertyMapping
-class RACPropertyMappingSerializer(PropertyMappingSerializer):
+class RACPropertyMappingSerializer(EnterpriseRequiredMixin, PropertyMappingSerializer):
"""RACPropertyMapping Serializer"""
static_settings = JSONDictField()
diff --git a/authentik/enterprise/providers/rac/api/providers.py b/authentik/enterprise/providers/rac/api/providers.py
index 6dd4f9f82..cda6c2af3 100644
--- a/authentik/enterprise/providers/rac/api/providers.py
+++ b/authentik/enterprise/providers/rac/api/providers.py
@@ -4,10 +4,11 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
+from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.models import RACProvider
-class RACProviderSerializer(ProviderSerializer):
+class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""RACProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
diff --git a/authentik/enterprise/providers/rac/apps.py b/authentik/enterprise/providers/rac/apps.py
index 973159bb9..13930faae 100644
--- a/authentik/enterprise/providers/rac/apps.py
+++ b/authentik/enterprise/providers/rac/apps.py
@@ -1,8 +1,8 @@
"""RAC app config"""
-from authentik.blueprints.apps import ManagedAppConfig
+from authentik.enterprise.apps import EnterpriseConfig
-class AuthentikEnterpriseProviderRAC(ManagedAppConfig):
+class AuthentikEnterpriseProviderRAC(EnterpriseConfig):
"""authentik enterprise rac app config"""
name = "authentik.enterprise.providers.rac"
diff --git a/authentik/enterprise/providers/rac/models.py b/authentik/enterprise/providers/rac/models.py
index d79bbd54c..f2806f32b 100644
--- a/authentik/enterprise/providers/rac/models.py
+++ b/authentik/enterprise/providers/rac/models.py
@@ -35,7 +35,7 @@ class AuthenticationMode(models.TextChoices):
class RACProvider(Provider):
- """Remotely access computers/servers"""
+ """Remotely access computers/servers via RDP/SSH/VNC."""
settings = models.JSONField(default=dict)
auth_mode = models.TextField(
diff --git a/schema.yml b/schema.yml
index 0ea6e8ee0..09b351707 100644
--- a/schema.yml
+++ b/schema.yml
@@ -19158,6 +19158,7 @@ paths:
- tr
- tt
- udm
+ - ug
- uk
- ur
- uz
@@ -42957,6 +42958,9 @@ components:
type: string
model_name:
type: string
+ requires_enterprise:
+ type: boolean
+ default: false
required:
- component
- description
diff --git a/web/src/admin/property-mappings/PropertyMappingWizard.ts b/web/src/admin/property-mappings/PropertyMappingWizard.ts
index 4773dd93a..4f0ab6122 100644
--- a/web/src/admin/property-mappings/PropertyMappingWizard.ts
+++ b/web/src/admin/property-mappings/PropertyMappingWizard.ts
@@ -13,21 +13,24 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
-import { CSSResult, TemplateResult, html } from "lit";
-import { property } from "lit/decorators.js";
+import { CSSResult, TemplateResult, html, nothing } from "lit";
+import { property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
-import { PropertymappingsApi, TypeCreate } from "@goauthentik/api";
+import { EnterpriseApi, LicenseSummary, PropertymappingsApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-property-mapping-wizard-initial")
export class InitialPropertyMappingWizardPage extends WizardPage {
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
+ @property({ attribute: false })
+ enterprise?: LicenseSummary;
+
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
@@ -60,11 +63,20 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
];
this.host.isValid = true;
}}
+ ?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false}
/>
${type.description}
+ ${type.requiresEnterprise && !this.enterprise?.hasLicense
+ ? html`
+
+ ${msg("Provider require enterprise.")}
+ ${msg("Learn more")}
+
+ `
+ : nothing}
`;
})}
`;
@@ -80,10 +92,16 @@ export class PropertyMappingWizard extends AKElement {
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
- firstUpdated(): void {
- new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList().then((types) => {
- this.mappingTypes = types;
- });
+ @state()
+ enterprise?: LicenseSummary;
+
+ async firstUpdated(): Promise {
+ this.mappingTypes = await new PropertymappingsApi(
+ DEFAULT_CONFIG,
+ ).propertymappingsAllTypesList();
+ this.enterprise = await new EnterpriseApi(
+ DEFAULT_CONFIG,
+ ).enterpriseLicenseSummaryRetrieve();
}
render(): TemplateResult {
diff --git a/web/src/admin/providers/ProviderWizard.ts b/web/src/admin/providers/ProviderWizard.ts
index a65945354..7f19b4d02 100644
--- a/web/src/admin/providers/ProviderWizard.ts
+++ b/web/src/admin/providers/ProviderWizard.ts
@@ -4,6 +4,7 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderImportForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
@@ -13,8 +14,8 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
-import { CSSResult, TemplateResult, html } from "lit";
-import { property } from "lit/decorators.js";
+import { CSSResult, TemplateResult, html, nothing } from "lit";
+import { property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@@ -22,13 +23,16 @@ import PFHint from "@patternfly/patternfly/components/Hint/hint.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
-import { ProvidersApi, TypeCreate } from "@goauthentik/api";
+import { EnterpriseApi, LicenseSummary, ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard-initial")
export class InitialProviderWizardPage extends WizardPage {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
+ @property({ attribute: false })
+ enterprise?: LicenseSummary;
+
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFHint, PFButton, PFRadio];
}
@@ -79,9 +83,18 @@ export class InitialProviderWizardPage extends WizardPage {
this.host.steps = ["initial", `type-${type.component}`];
this.host.isValid = true;
}}
+ ?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false}
/>
${type.description}
+ ${type.requiresEnterprise && !this.enterprise?.hasLicense
+ ? html`
+
+ ${msg("Provider require enterprise.")}
+ ${msg("Learn more")}
+
+ `
+ : nothing}
`;
})}
`;
@@ -100,15 +113,19 @@ export class ProviderWizard extends AKElement {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
+ @state()
+ enterprise?: LicenseSummary;
+
@property({ attribute: false })
finalHandler: () => Promise = () => {
return Promise.resolve();
};
- firstUpdated(): void {
- new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
- this.providerTypes = types;
- });
+ async firstUpdated(): Promise {
+ this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
+ this.enterprise = await new EnterpriseApi(
+ DEFAULT_CONFIG,
+ ).enterpriseLicenseSummaryRetrieve();
}
render(): TemplateResult {
@@ -121,7 +138,11 @@ export class ProviderWizard extends AKElement {
return this.finalHandler();
}}
>
-
+
${this.providerTypes.map((type) => {
return html`
diff --git a/web/src/elements/enterprise/EnterpriseStatusBanner.ts b/web/src/elements/enterprise/EnterpriseStatusBanner.ts
index 0ac115457..09d376759 100644
--- a/web/src/elements/enterprise/EnterpriseStatusBanner.ts
+++ b/web/src/elements/enterprise/EnterpriseStatusBanner.ts
@@ -21,10 +21,8 @@ export class EnterpriseStatusBanner extends AKElement {
return [PFBanner];
}
- firstUpdated(): void {
- new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((b) => {
- this.summary = b;
- });
+ async firstUpdated(): Promise {
+ this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve();
}
renderBanner(): TemplateResult {
diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf
index 8cac9d9de..403d08256 100644
--- a/web/xliff/de.xlf
+++ b/web/xliff/de.xlf
@@ -6237,6 +6237,12 @@ Bindings to groups/users are checked against the user of the event.
+
+
+
+
+
+