enterprise: initial enterprise (#5721)
* initial Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add external users Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ui, add more logic, add public JWT validation key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert to not use install_id as session jwt signing key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * switch to PKI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more licensing stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add install ID to form Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use x5c correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * license checks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use production CA Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more UI stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to summary Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update locale, improve ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add direct button Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update link Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format and such Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove old attributes from ldap Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove is_enterprise_licensed Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix admin interface styling issue Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Update authentik/core/models.py Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * fix default case Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
parent
cf799fca03
commit
41af486006
|
@ -204,3 +204,4 @@ data/
|
|||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.ruff_cache
|
||||
|
|
|
@ -9,7 +9,7 @@ from rest_framework.exceptions import AuthenticationFailed
|
|||
|
||||
from authentik.api.authentication import bearer_auth
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
|
||||
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||
|
@ -57,8 +57,8 @@ class TestAPIAuth(TestCase):
|
|||
@reconcile_app("authentik_outposts")
|
||||
def test_managed_outpost_success(self):
|
||||
"""Test managed outpost"""
|
||||
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)
|
||||
user: User = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
self.assertEqual(user.type, UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
|
||||
def test_jwt_valid(self):
|
||||
"""Test valid JWT"""
|
||||
|
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
|||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.dispatch import Signal
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
|
@ -21,6 +22,8 @@ from authentik.core.api.utils import PassiveSerializer
|
|||
from authentik.events.geo import GEOIP_READER
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
capabilities = Signal()
|
||||
|
||||
|
||||
class Capabilities(models.TextChoices):
|
||||
"""Define capabilities which influence which APIs can/should be used"""
|
||||
|
@ -73,6 +76,9 @@ class ConfigView(APIView):
|
|||
caps.append(Capabilities.CAN_DEBUG)
|
||||
if "authentik.enterprise" in settings.INSTALLED_APPS:
|
||||
caps.append(Capabilities.IS_ENTERPRISE)
|
||||
for _, result in capabilities.send(sender=self):
|
||||
if result:
|
||||
caps.append(result)
|
||||
return caps
|
||||
|
||||
def get_config(self) -> ConfigSerializer:
|
||||
|
|
|
@ -59,7 +59,6 @@ from authentik.core.middleware import (
|
|||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_PATH_SERVICE_ACCOUNT,
|
||||
AuthenticatedSession,
|
||||
|
@ -67,6 +66,7 @@ from authentik.core.models import (
|
|||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
|
@ -147,6 +147,18 @@ class UserSerializer(ModelSerializer):
|
|||
raise ValidationError(_("No empty segments in user path allowed."))
|
||||
return path
|
||||
|
||||
def validate_type(self, user_type: str) -> str:
|
||||
"""Validate user type, internal_service_account is an internal value"""
|
||||
if (
|
||||
self.instance
|
||||
and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
and user_type != UserTypes.INTERNAL_SERVICE_ACCOUNT.value
|
||||
):
|
||||
raise ValidationError("Can't change internal service account to other user type.")
|
||||
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
|
||||
raise ValidationError("Setting a user to internal service account is not allowed.")
|
||||
return user_type
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
|
@ -163,6 +175,7 @@ class UserSerializer(ModelSerializer):
|
|||
"attributes",
|
||||
"uid",
|
||||
"path",
|
||||
"type",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"allow_blank": True},
|
||||
|
@ -211,6 +224,7 @@ class UserSelfSerializer(ModelSerializer):
|
|||
"avatar",
|
||||
"uid",
|
||||
"settings",
|
||||
"type",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"is_active": {"read_only": True},
|
||||
|
@ -329,6 +343,7 @@ class UsersFilter(FilterSet):
|
|||
"attributes",
|
||||
"groups_by_name",
|
||||
"groups_by_pk",
|
||||
"type",
|
||||
]
|
||||
|
||||
|
||||
|
@ -421,7 +436,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||
user: User = User.objects.create(
|
||||
username=username,
|
||||
name=username,
|
||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
|
||||
path=USER_PATH_SERVICE_ACCOUNT,
|
||||
)
|
||||
user.set_unusable_password()
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 4.1.7 on 2023-05-21 11:44
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_user_type(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
|
||||
from authentik.core.models import UserTypes
|
||||
|
||||
for user in User.objects.using(db_alias).all():
|
||||
user.type = UserTypes.DEFAULT
|
||||
if "goauthentik.io/user/service-account" in user.attributes:
|
||||
user.type = UserTypes.SERVICE_ACCOUNT
|
||||
if "goauthentik.io/user/override-ips" in user.attributes:
|
||||
user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_core", "0029_provider_backchannel_applications_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="type",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("default", "Default"),
|
||||
("external", "External"),
|
||||
("service_account", "Service Account"),
|
||||
("internal_service_account", "Internal Service Account"),
|
||||
],
|
||||
default="default",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_user_type),
|
||||
]
|
|
@ -36,7 +36,6 @@ from authentik.root.install_id import get_install_id
|
|||
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
||||
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||
|
@ -45,8 +44,6 @@ USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
|||
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||
|
||||
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||
|
||||
|
@ -66,6 +63,21 @@ def default_token_key():
|
|||
return generate_id(int(CONFIG.y("default_token_length")))
|
||||
|
||||
|
||||
class UserTypes(models.TextChoices):
|
||||
"""User types, both for grouping, licensing and permissions in the case
|
||||
of the internal_service_account"""
|
||||
|
||||
DEFAULT = "default"
|
||||
EXTERNAL = "external"
|
||||
|
||||
# User-created service accounts
|
||||
SERVICE_ACCOUNT = "service_account"
|
||||
|
||||
# Special user type for internally managed and created service
|
||||
# accounts, such as outpost users
|
||||
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
|
||||
|
||||
|
||||
class Group(SerializerModel):
|
||||
"""Custom Group model which supports a basic hierarchy"""
|
||||
|
||||
|
@ -149,6 +161,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
|
||||
name = models.TextField(help_text=_("User's display name."))
|
||||
path = models.TextField(default="users")
|
||||
type = models.TextField(choices=UserTypes.choices, default=UserTypes.DEFAULT)
|
||||
|
||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""authentik core signals"""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
|
@ -10,16 +8,13 @@ from django.db.models.signals import post_save, pre_delete, pre_save
|
|||
from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider
|
||||
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
||||
login_failed = Signal()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
@receiver(post_save, sender=Application)
|
||||
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||
|
@ -35,7 +30,7 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
|
|||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
||||
def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
||||
"""Create an AuthenticatedSession from request"""
|
||||
|
||||
session = AuthenticatedSession.from_request(request, user)
|
||||
|
@ -44,7 +39,7 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
|||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
||||
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
||||
"""Delete AuthenticatedSession if it exists"""
|
||||
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ from django.urls.base import reverse
|
|||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
AuthenticatedSession,
|
||||
Token,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||
from authentik.flows.models import FlowDesignation
|
||||
|
@ -141,7 +141,8 @@ class TestUsersAPI(APITestCase):
|
|||
|
||||
user_filter = User.objects.filter(
|
||||
username="test-sa",
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
|
||||
)
|
||||
self.assertTrue(user_filter.exists())
|
||||
user: User = user_filter.first()
|
||||
|
@ -166,7 +167,8 @@ class TestUsersAPI(APITestCase):
|
|||
|
||||
user_filter = User.objects.filter(
|
||||
username="test-sa",
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||
)
|
||||
self.assertTrue(user_filter.exists())
|
||||
user: User = user_filter.first()
|
||||
|
@ -192,7 +194,8 @@ class TestUsersAPI(APITestCase):
|
|||
|
||||
user_filter = User.objects.filter(
|
||||
username="test-sa",
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
|
||||
)
|
||||
self.assertTrue(user_filter.exists())
|
||||
user: User = user_filter.first()
|
||||
|
@ -218,7 +221,8 @@ class TestUsersAPI(APITestCase):
|
|||
|
||||
user_filter = User.objects.filter(
|
||||
username="test-sa",
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
|
||||
)
|
||||
self.assertTrue(user_filter.exists())
|
||||
user: User = user_filter.first()
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
"""Enterprise API Views"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, DateTimeField, IntegerField
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.enterprise.models import License, LicenseKey
|
||||
from authentik.root.install_id import get_install_id
|
||||
|
||||
|
||||
class LicenseSerializer(ModelSerializer):
|
||||
"""License Serializer"""
|
||||
|
||||
def validate_key(self, key: str) -> str:
|
||||
"""Validate the license key (install_id and signature)"""
|
||||
LicenseKey.validate(key)
|
||||
return key
|
||||
|
||||
class Meta:
|
||||
model = License
|
||||
fields = [
|
||||
"license_uuid",
|
||||
"name",
|
||||
"key",
|
||||
"expiry",
|
||||
"users",
|
||||
"external_users",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"read_only": True},
|
||||
"expiry": {"read_only": True},
|
||||
"users": {"read_only": True},
|
||||
"external_users": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class LicenseSummary(PassiveSerializer):
|
||||
"""Serializer for license status"""
|
||||
|
||||
users = IntegerField(required=True)
|
||||
external_users = IntegerField(required=True)
|
||||
valid = BooleanField()
|
||||
show_admin_warning = BooleanField()
|
||||
show_user_warning = BooleanField()
|
||||
read_only = BooleanField()
|
||||
latest_valid = DateTimeField()
|
||||
has_license = BooleanField()
|
||||
|
||||
|
||||
class LicenseForecastSerializer(PassiveSerializer):
|
||||
"""Serializer for license forecast"""
|
||||
|
||||
users = IntegerField(required=True)
|
||||
external_users = IntegerField(required=True)
|
||||
|
||||
|
||||
class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||
"""License Viewset"""
|
||||
|
||||
queryset = License.objects.all()
|
||||
serializer_class = LicenseSerializer
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
|
||||
@permission_required(None, ["authentik_enterprise.view_license"])
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
200: inline_serializer("InstallIDSerializer", {"install_id": CharField(required=True)}),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["GET"], permission_classes=[IsAdminUser])
|
||||
def get_install_id(self, request: Request) -> Response:
|
||||
"""Get install_id"""
|
||||
return Response(
|
||||
data={
|
||||
"install_id": get_install_id(),
|
||||
}
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
200: LicenseSummary(),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["GET"], permission_classes=[IsAuthenticated])
|
||||
def summary(self, request: Request) -> Response:
|
||||
"""Get the total license status"""
|
||||
total = LicenseKey.get_total()
|
||||
last_valid = LicenseKey.last_valid_date()
|
||||
# TODO: move this to a different place?
|
||||
show_admin_warning = last_valid < now() - timedelta(weeks=2)
|
||||
show_user_warning = last_valid < now() - timedelta(weeks=4)
|
||||
read_only = last_valid < now() - timedelta(weeks=6)
|
||||
latest_valid = datetime.fromtimestamp(total.exp)
|
||||
response = LicenseSummary(
|
||||
data={
|
||||
"users": total.users,
|
||||
"external_users": total.external_users,
|
||||
"valid": total.is_valid(),
|
||||
"show_admin_warning": show_admin_warning,
|
||||
"show_user_warning": show_user_warning,
|
||||
"read_only": read_only,
|
||||
"latest_valid": latest_valid,
|
||||
"has_license": License.objects.all().count() > 0,
|
||||
}
|
||||
)
|
||||
response.is_valid(raise_exception=True)
|
||||
return Response(response.data)
|
||||
|
||||
@permission_required(None, ["authentik_enterprise.view_license"])
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
200: LicenseForecastSerializer(),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["GET"])
|
||||
def forecast(self, request: Request) -> Response:
|
||||
"""Forecast how many users will be required in a year"""
|
||||
last_month = now() - timedelta(days=30)
|
||||
# Forecast for default users
|
||||
users_in_last_month = User.objects.filter(
|
||||
type=UserTypes.DEFAULT, date_joined__gte=last_month
|
||||
).count()
|
||||
# Forecast for external users
|
||||
external_in_last_month = LicenseKey.get_external_user_count()
|
||||
forecast_for_months = 12
|
||||
response = LicenseForecastSerializer(
|
||||
data={
|
||||
"users": users_in_last_month * forecast_for_months,
|
||||
"external_users": external_in_last_month * forecast_for_months,
|
||||
}
|
||||
)
|
||||
response.is_valid(raise_exception=True)
|
||||
return Response(response.data)
|
|
@ -9,3 +9,7 @@ class AuthentikEnterpriseConfig(ManagedAppConfig):
|
|||
label = "authentik_enterprise"
|
||||
verbose_name = "authentik Enterprise"
|
||||
default = True
|
||||
|
||||
def reconcile_load_enterprise_signals(self):
|
||||
"""Load enterprise signals"""
|
||||
self.import_module("authentik.enterprise.signals")
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.1.10 on 2023-07-06 12:51
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.enterprise.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="License",
|
||||
fields=[
|
||||
(
|
||||
"license_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("key", models.TextField(unique=True)),
|
||||
("name", models.TextField()),
|
||||
("expiry", models.DateTimeField()),
|
||||
("users", models.BigIntegerField()),
|
||||
("external_users", models.BigIntegerField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LicenseUsage",
|
||||
fields=[
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
("expires", models.DateTimeField(default=authentik.enterprise.models.usage_expiry)),
|
||||
(
|
||||
"usage_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("user_count", models.BigIntegerField()),
|
||||
("external_user_count", models.BigIntegerField()),
|
||||
("within_limits", models.BooleanField()),
|
||||
("record_date", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,185 @@
|
|||
"""Enterprise models"""
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from time import mktime
|
||||
from uuid import uuid4
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
||||
from dacite import from_dict
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from jwt import PyJWTError, decode, get_unverified_header
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.models import ExpiringModel, User, UserTypes
|
||||
from authentik.root.install_id import get_install_id
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_licensing_key() -> Certificate:
|
||||
"""Get Root CA PEM"""
|
||||
with open("authentik/enterprise/public.pem", "rb") as _key:
|
||||
return load_pem_x509_certificate(_key.read())
|
||||
|
||||
|
||||
def get_license_aud() -> str:
|
||||
"""Get the JWT audience field"""
|
||||
return f"enterprise.goauthentik.io/license/{get_install_id()}"
|
||||
|
||||
|
||||
class LicenseFlags(Enum):
|
||||
"""License flags"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseKey:
|
||||
"""License JWT claims"""
|
||||
|
||||
aud: str
|
||||
exp: int
|
||||
|
||||
name: str
|
||||
users: int
|
||||
external_users: int
|
||||
flags: list[LicenseFlags] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def validate(jwt: str) -> "LicenseKey":
|
||||
"""Validate the license from a given JWT"""
|
||||
try:
|
||||
headers = get_unverified_header(jwt)
|
||||
except PyJWTError:
|
||||
raise ValidationError("Unable to verify license")
|
||||
x5c: list[str] = headers.get("x5c", [])
|
||||
if len(x5c) < 1:
|
||||
raise ValidationError("Unable to verify license")
|
||||
try:
|
||||
our_cert = load_pem_x509_certificate(b64decode(x5c[0]))
|
||||
intermediate = load_pem_x509_certificate(b64decode(x5c[1]))
|
||||
our_cert.verify_directly_issued_by(intermediate)
|
||||
intermediate.verify_directly_issued_by(get_licensing_key())
|
||||
except (InvalidSignature, TypeError, ValueError, Error):
|
||||
raise ValidationError("Unable to verify license")
|
||||
try:
|
||||
body = from_dict(
|
||||
LicenseKey,
|
||||
decode(
|
||||
jwt,
|
||||
our_cert.public_key(),
|
||||
algorithms=["ES521"],
|
||||
audience=get_license_aud(),
|
||||
),
|
||||
)
|
||||
except PyJWTError:
|
||||
raise ValidationError("Unable to verify license")
|
||||
return body
|
||||
|
||||
@staticmethod
|
||||
def get_total() -> "LicenseKey":
|
||||
"""Get a summarized version of all (not expired) licenses"""
|
||||
active_licenses = License.objects.filter(expiry__gte=now())
|
||||
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
|
||||
for lic in active_licenses:
|
||||
total.users += lic.users
|
||||
total.external_users += lic.external_users
|
||||
exp_ts = int(mktime(lic.expiry.timetuple()))
|
||||
if total.exp == 0:
|
||||
total.exp = exp_ts
|
||||
if exp_ts <= total.exp:
|
||||
total.exp = exp_ts
|
||||
total.flags.extend(lic.status.flags)
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def base_user_qs() -> QuerySet:
|
||||
"""Base query set for all users"""
|
||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||
|
||||
@staticmethod
|
||||
def get_default_user_count():
|
||||
"""Get current default user count"""
|
||||
return LicenseKey.base_user_qs().filter(type=UserTypes.DEFAULT).count()
|
||||
|
||||
@staticmethod
|
||||
def get_external_user_count():
|
||||
"""Get current external user count"""
|
||||
# Count since start of the month
|
||||
last_month = now().replace(day=1)
|
||||
return (
|
||||
LicenseKey.base_user_qs()
|
||||
.filter(type=UserTypes.EXTERNAL, last_login__gte=last_month)
|
||||
.count()
|
||||
)
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if the given license body covers all users
|
||||
|
||||
Only checks the current count, no historical data is checked"""
|
||||
default_users = self.get_default_user_count()
|
||||
if default_users > self.users:
|
||||
return False
|
||||
active_users = self.get_external_user_count()
|
||||
if active_users > self.external_users:
|
||||
return False
|
||||
return True
|
||||
|
||||
def record_usage(self):
|
||||
"""Capture the current validity status and metrics and save them"""
|
||||
LicenseUsage.objects.create(
|
||||
user_count=self.get_default_user_count(),
|
||||
external_user_count=self.get_external_user_count(),
|
||||
within_limits=self.is_valid(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def last_valid_date() -> datetime:
|
||||
"""Get the last date the license was valid"""
|
||||
usage: LicenseUsage = (
|
||||
LicenseUsage.filter_not_expired(within_limits=True).order_by("-record_date").first()
|
||||
)
|
||||
if not usage:
|
||||
return now()
|
||||
return usage.record_date
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
"""An authentik enterprise license"""
|
||||
|
||||
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
key = models.TextField(unique=True)
|
||||
|
||||
name = models.TextField()
|
||||
expiry = models.DateTimeField()
|
||||
users = models.BigIntegerField()
|
||||
external_users = models.BigIntegerField()
|
||||
|
||||
@property
|
||||
def status(self) -> LicenseKey:
|
||||
"""Get parsed license status"""
|
||||
return LicenseKey.validate(self.key)
|
||||
|
||||
|
||||
def usage_expiry():
|
||||
"""Keep license usage records for 3 months"""
|
||||
return now() + timedelta(days=30 * 3)
|
||||
|
||||
|
||||
class LicenseUsage(ExpiringModel):
|
||||
"""a single license usage record"""
|
||||
|
||||
expires = models.DateTimeField(default=usage_expiry)
|
||||
|
||||
usage_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
user_count = models.BigIntegerField()
|
||||
external_user_count = models.BigIntegerField()
|
||||
within_limits = models.BooleanField()
|
||||
|
||||
record_date = models.DateTimeField(auto_now_add=True)
|
|
@ -0,0 +1,46 @@
|
|||
"""Enterprise license policies"""
|
||||
from typing import Optional
|
||||
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.enterprise.models import LicenseKey
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
||||
|
||||
class EnterprisePolicy(Policy):
|
||||
"""Check that a user is correctly licensed for the request"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
raise NotImplementedError
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
if not LicenseKey.get_total().is_valid():
|
||||
return PolicyResult(False)
|
||||
if request.user.type != UserTypes.DEFAULT:
|
||||
return PolicyResult(False)
|
||||
return PolicyResult(True)
|
||||
|
||||
|
||||
class EnterprisePolicyAccessView(PolicyAccessView):
|
||||
"""PolicyAccessView which also checks enterprise licensing"""
|
||||
|
||||
def user_has_access(self, user: Optional[User] = None) -> PolicyResult:
|
||||
user = user or self.request.user
|
||||
request = PolicyRequest(user)
|
||||
request.http_request = self.request
|
||||
result = super().user_has_access(user)
|
||||
enterprise_result = EnterprisePolicy().passes(request)
|
||||
if not enterprise_result.passing:
|
||||
return enterprise_result
|
||||
return result
|
||||
|
||||
def resolve_provider_application(self):
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,26 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEdzCCA/6gAwIBAgIUQrj1jxn4q/BB38B2SwTrvGyrZLMwCgYIKoZIzj0EAwMw
|
||||
ge8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T
|
||||
YW4gRnJhbmNpc2NvMSQwIgYDVQQJExs1NDggTWFya2V0IFN0cmVldCBQbWIgNzAx
|
||||
NDgxDjAMBgNVBBETBTk0MTA0MSAwHgYDVQQKExdBdXRoZW50aWsgU2VjdXJpdHkg
|
||||
SW5jLjEcMBoGA1UECxMTRW50ZXJwcmlzZSBMaWNlbnNlczE9MDsGA1UEAxM0QXV0
|
||||
aGVudGlrIFNlY3VyaXR5IEluYy4gRW50ZXJwcmlzZSBMaWNlbnNpbmcgUm9vdCBY
|
||||
MTAgFw0yMzA3MDQxNzQ3NDBaGA8yMTIzMDYxMDE3NDgxMFowge8xCzAJBgNVBAYT
|
||||
AlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv
|
||||
MSQwIgYDVQQJExs1NDggTWFya2V0IFN0cmVldCBQbWIgNzAxNDgxDjAMBgNVBBET
|
||||
BTk0MTA0MSAwHgYDVQQKExdBdXRoZW50aWsgU2VjdXJpdHkgSW5jLjEcMBoGA1UE
|
||||
CxMTRW50ZXJwcmlzZSBMaWNlbnNlczE9MDsGA1UEAxM0QXV0aGVudGlrIFNlY3Vy
|
||||
aXR5IEluYy4gRW50ZXJwcmlzZSBMaWNlbnNpbmcgUm9vdCBYMTB2MBAGByqGSM49
|
||||
AgEGBSuBBAAiA2IABNbPJH6nDbSshpDsDHBRL0UcZVXWCK30txqcMKU+YFmLB6iR
|
||||
PJiHjHA8Z+5aP4eNH6onA5xqykQf65tvbFBA1LB/6HqMArU/tYVVQx4+o9hRBxF5
|
||||
RrzXucUg2br+RX8aa6OCAVUwggFRMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
|
||||
BTADAQH/MB0GA1UdDgQWBBRHpR3/ptPgN0yHVfUjyJOEmsPZqTAfBgNVHSMEGDAW
|
||||
gBRHpR3/ptPgN0yHVfUjyJOEmsPZqTCBoAYIKwYBBQUHAQEEgZMwgZAwRwYIKwYB
|
||||
BQUHMAGGO2h0dHBzOi8vdmF1bHQuY3VzdG9tZXJzLmdvYXV0aGVudGlrLmlvL3Yx
|
||||
L2xpY2Vuc2luZy1jYS9vY3NwMEUGCCsGAQUFBzAChjlodHRwczovL3ZhdWx0LmN1
|
||||
c3RvbWVycy5nb2F1dGhlbnRpay5pby92MS9saWNlbnNpbmctY2EvY2EwSwYDVR0f
|
||||
BEQwQjBAoD6gPIY6aHR0cHM6Ly92YXVsdC5jdXN0b21lcnMuZ29hdXRoZW50aWsu
|
||||
aW8vdjEvbGljZW5zaW5nLWNhL2NybDAKBggqhkjOPQQDAwNnADBkAjB0+YA1yjEO
|
||||
g43CCYUJXz9m9CNIkjOPUI0jO4UtvSj8j067TKRbX6IL/29HxPtQoYACME8eZHBJ
|
||||
Ljcog0oeBgjr4wK8bobgknr5wrm70rrNNpbSAjDvTvXMQeAShGgsftEquQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -1 +1,12 @@
|
|||
"""Enterprise additional settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"enterprise_calculate_license": {
|
||||
"task": "authentik.enterprise.tasks.calculate_license",
|
||||
"schedule": crontab(minute=fqdn_rand("calculate_license"), hour="*/8"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
"""Enterprise signals"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import get_current_timezone
|
||||
|
||||
from authentik.enterprise.models import License
|
||||
|
||||
|
||||
@receiver(pre_save, sender=License)
|
||||
def pre_save_license(sender: type[License], instance: License, **_):
|
||||
"""Extract data from license jwt and save it into model"""
|
||||
status = instance.status
|
||||
instance.name = status.name
|
||||
instance.users = status.users
|
||||
instance.external_users = status.external_users
|
||||
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())
|
|
@ -0,0 +1,10 @@
|
|||
"""Enterprise tasks"""
|
||||
from authentik.enterprise.models import LicenseKey
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def calculate_license():
|
||||
"""Calculate licensing status"""
|
||||
total = LicenseKey.get_total()
|
||||
total.record_usage()
|
|
@ -0,0 +1,64 @@
|
|||
"""Enterprise license tests"""
|
||||
from datetime import timedelta
|
||||
from time import mktime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.enterprise.models import License, LicenseKey
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
_exp = int(mktime((now() + timedelta(days=3000)).timetuple()))
|
||||
|
||||
|
||||
class TestEnterpriseLicense(TestCase):
|
||||
"""Enterprise license tests"""
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.models.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=_exp,
|
||||
name=generate_id(),
|
||||
users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_valid(self):
|
||||
"""Check license verification"""
|
||||
lic = License.objects.create(key=generate_id())
|
||||
self.assertTrue(lic.status.is_valid())
|
||||
self.assertEqual(lic.users, 100)
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test invalid license"""
|
||||
with self.assertRaises(ValidationError):
|
||||
License.objects.create(key=generate_id())
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.models.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=_exp,
|
||||
name=generate_id(),
|
||||
users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_valid_multiple(self):
|
||||
"""Check license verification"""
|
||||
lic = License.objects.create(key=generate_id())
|
||||
self.assertTrue(lic.status.is_valid())
|
||||
lic2 = License.objects.create(key=generate_id())
|
||||
self.assertTrue(lic2.status.is_valid())
|
||||
total = LicenseKey.get_total()
|
||||
self.assertEqual(total.users, 200)
|
||||
self.assertEqual(total.external_users, 200)
|
||||
self.assertEqual(total.exp, _exp)
|
||||
self.assertTrue(total.is_valid())
|
|
@ -0,0 +1,7 @@
|
|||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.api import LicenseViewSet
|
||||
|
||||
api_urlpatterns = [
|
||||
("enterprise/license", LicenseViewSet),
|
||||
]
|
|
@ -1,7 +1,7 @@
|
|||
"""Test HTTP Helpers"""
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents
|
||||
from authentik.core.models import Token, TokenIntents, UserTypes
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.utils.http import OUTPOST_REMOTE_IP_HEADER, OUTPOST_TOKEN_HEADER, get_client_ip
|
||||
from authentik.lib.views import bad_request_message
|
||||
|
@ -53,7 +53,7 @@ class TestHTTP(TestCase):
|
|||
)
|
||||
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
||||
# Valid
|
||||
self.user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
self.user.save()
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
|
|
|
@ -33,9 +33,8 @@ def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
|
|||
|
||||
def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
||||
"""Get the actual remote IP when set by an outpost. Only
|
||||
allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set
|
||||
to outpost"""
|
||||
from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents
|
||||
allowed when the request is authenticated, by an outpost internal service account"""
|
||||
from authentik.core.models import Token, TokenIntents, UserTypes
|
||||
|
||||
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
|
||||
return None
|
||||
|
@ -51,7 +50,7 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
|||
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
|
||||
return None
|
||||
user = token.user
|
||||
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
if user.type != UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
LOGGER.warning(
|
||||
"Remote-IP override: user doesn't have permission",
|
||||
user=user,
|
||||
|
|
|
@ -20,13 +20,12 @@ from structlog.stdlib import get_logger
|
|||
from authentik import __version__, get_build_hash
|
||||
from authentik.blueprints.models import ManagedModel
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_PATH_SYSTEM_PREFIX,
|
||||
Provider,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
@ -346,8 +345,7 @@ class Outpost(SerializerModel, ManagedModel):
|
|||
user: User = User.objects.create(username=self.user_identifier)
|
||||
user.set_unusable_password()
|
||||
user_created = True
|
||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||
user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
user.name = f"Outpost {self.name} Service-Account"
|
||||
user.path = USER_PATH_OUTPOSTS
|
||||
user.save()
|
||||
|
|
|
@ -64,7 +64,7 @@ class PolicyEngine:
|
|||
self.use_cache = True
|
||||
self.__expected_result_count = 0
|
||||
|
||||
def _iter_bindings(self) -> Iterator[PolicyBinding]:
|
||||
def iterate_bindings(self) -> Iterator[PolicyBinding]:
|
||||
"""Make sure all Policies are their respective classes"""
|
||||
return (
|
||||
PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
|
||||
|
@ -88,7 +88,7 @@ class PolicyEngine:
|
|||
span: Span
|
||||
span.set_data("pbm", self.__pbm)
|
||||
span.set_data("request", self.request)
|
||||
for binding in self._iter_bindings():
|
||||
for binding in self.iterate_bindings():
|
||||
self.__expected_result_count += 1
|
||||
|
||||
self._check_policy_type(binding)
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
|||
from jwt import decode
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
|
||||
from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.constants import (
|
||||
|
@ -37,7 +37,7 @@ class TestTokenClientCredentials(OAuthTestCase):
|
|||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
|
||||
self.user = create_test_admin_user("sa")
|
||||
self.user.attributes[USER_ATTRIBUTE_SA] = True
|
||||
self.user.type = UserTypes.SERVICE_ACCOUNT
|
||||
self.user.save()
|
||||
self.token = Token.objects.create(
|
||||
identifier="sa-token",
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
"""SCIM Provider models"""
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
BackchannelProvider,
|
||||
Group,
|
||||
PropertyMapping,
|
||||
User,
|
||||
)
|
||||
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
|
||||
|
||||
|
||||
class SCIMProvider(BackchannelProvider):
|
||||
|
@ -38,17 +32,8 @@ class SCIMProvider(BackchannelProvider):
|
|||
according to the provider's settings"""
|
||||
base = User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.filter(
|
||||
Q(
|
||||
**{
|
||||
f"attributes__{USER_ATTRIBUTE_SA}__isnull": True,
|
||||
}
|
||||
)
|
||||
| Q(
|
||||
**{
|
||||
f"attributes__{USER_ATTRIBUTE_SA}": False,
|
||||
}
|
||||
)
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
)
|
||||
if self.filter_group:
|
||||
base = base.filter(ak_groups__in=[self.filter_group])
|
||||
|
|
|
@ -146,6 +146,7 @@ SPECTACULAR_SETTINGS = {
|
|||
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
|
||||
"LDAPAPIAccessMode": "authentik.providers.ldap.models.APIAccessMode",
|
||||
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
|
||||
"UserTypeEnum": "authentik.core.models.UserTypes",
|
||||
},
|
||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||
"POSTPROCESSING_HOOKS": [
|
||||
|
|
|
@ -179,7 +179,7 @@ class ListPolicyEngine(PolicyEngine):
|
|||
self.__list = policies
|
||||
self.use_cache = False
|
||||
|
||||
def _iter_bindings(self) -> Iterator[PolicyBinding]:
|
||||
def iterate_bindings(self) -> Iterator[PolicyBinding]:
|
||||
for policy in self.__list:
|
||||
yield PolicyBinding(
|
||||
policy=policy,
|
||||
|
|
|
@ -3980,6 +3980,16 @@
|
|||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -4171,6 +4181,16 @@
|
|||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -4366,6 +4386,16 @@
|
|||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -6522,6 +6552,16 @@
|
|||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -7257,6 +7297,16 @@
|
|||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -8334,6 +8384,16 @@
|
|||
"minLength": 1,
|
||||
"title": "Path"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"external",
|
||||
"service_account",
|
||||
"internal_service_account"
|
||||
],
|
||||
"title": "Type"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
|
|
|
@ -41,7 +41,7 @@ func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearc
|
|||
// Custom attributes
|
||||
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base
|
||||
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
|
||||
"( 1.3.6.1.4.1.26027.1.1.1 NAME 'goauthentik.io/ldap/user' SUP organizationalPerson STRUCTURAL MAY ( ak-active $ sAMAccountName $ goauthentikio-user-sources $ goauthentik.io/user/sources $ goauthentik.io/ldap/active $ goauthentik.io/ldap/superuser $ goauthentikio-user-override-ips $ goauthentikio-user-service-account ) )",
|
||||
"( 1.3.6.1.4.1.26027.1.1.1 NAME 'goauthentik.io/ldap/user' SUP organizationalPerson STRUCTURAL MAY ( ak-active $ sAMAccountName $ goauthentikio-user-sources $ goauthentik.io/user/sources $ goauthentik.io/ldap/active $ goauthentik.io/ldap/superuser ) )",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -85,8 +85,6 @@ func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearc
|
|||
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
|
||||
"( 1.3.6.1.4.1.26027.1.1.2 NAME ( 'goauthentik.io/ldap/superuser' 'ak-superuser' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||
"( 1.3.6.1.4.1.26027.1.1.3 NAME ( 'goauthentik.io/ldap/active' 'ak-active' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||
"( 1.3.6.1.4.1.26027.1.1.4 NAME 'goauthentikio-user-override-ips' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||
"( 1.3.6.1.4.1.26027.1.1.5 NAME 'goauthentikio-user-service-account' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE' )",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-11 18:34+0000\n"
|
||||
"POT-Creation-Date: 2023-07-16 13:59+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -90,133 +90,133 @@ msgstr ""
|
|||
msgid "No empty segments in user path allowed."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:74
|
||||
#: authentik/core/models.py:86
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:76
|
||||
#: authentik/core/models.py:88
|
||||
msgid "Users added to this group will be superusers."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:150
|
||||
#: authentik/core/models.py:162
|
||||
msgid "User's display name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:243 authentik/providers/oauth2/models.py:294
|
||||
#: authentik/core/models.py:256 authentik/providers/oauth2/models.py:294
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:244
|
||||
#: authentik/core/models.py:257
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:257
|
||||
#: authentik/core/models.py:270
|
||||
msgid ""
|
||||
"Flow used for authentication when the associated application is accessed by "
|
||||
"an un-authenticated user."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:267
|
||||
#: authentik/core/models.py:280
|
||||
msgid "Flow used when authorizing this provider."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:279
|
||||
#: authentik/core/models.py:292
|
||||
msgid ""
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
"like LDAP and SCIM."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:334
|
||||
#: authentik/core/models.py:347
|
||||
msgid "Application's display Name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:335
|
||||
#: authentik/core/models.py:348
|
||||
msgid "Internal application name, used in URLs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:347
|
||||
#: authentik/core/models.py:360
|
||||
msgid "Open launch URL in a new browser tab or window."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:411
|
||||
#: authentik/core/models.py:424
|
||||
msgid "Application"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:412
|
||||
#: authentik/core/models.py:425
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:418
|
||||
#: authentik/core/models.py:431
|
||||
msgid "Use the source-specific identifier"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:420
|
||||
#: authentik/core/models.py:433
|
||||
msgid ""
|
||||
"Link to a user with identical email address. Can have security implications "
|
||||
"when a source doesn't validate email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:424
|
||||
#: authentik/core/models.py:437
|
||||
msgid ""
|
||||
"Use the user's email address, but deny enrollment when the email address "
|
||||
"already exists."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:427
|
||||
#: authentik/core/models.py:440
|
||||
msgid ""
|
||||
"Link to a user with identical username. Can have security implications when "
|
||||
"a username is used with another source."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:431
|
||||
#: authentik/core/models.py:444
|
||||
msgid ""
|
||||
"Use the user's username, but deny enrollment when the username already "
|
||||
"exists."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:438
|
||||
#: authentik/core/models.py:451
|
||||
msgid "Source's display Name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:439
|
||||
#: authentik/core/models.py:452
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:458
|
||||
#: authentik/core/models.py:471
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:467
|
||||
#: authentik/core/models.py:480
|
||||
msgid "Flow to use when enrolling new users."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:475
|
||||
#: authentik/core/models.py:488
|
||||
msgid ""
|
||||
"How the source determines if an existing user should be authenticated or a "
|
||||
"new user enrolled."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:647
|
||||
#: authentik/core/models.py:660
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:648
|
||||
#: authentik/core/models.py:661
|
||||
msgid "Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:689
|
||||
#: authentik/core/models.py:702
|
||||
msgid "Property Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:690
|
||||
#: authentik/core/models.py:703
|
||||
msgid "Property Mappings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:725
|
||||
#: authentik/core/models.py:738
|
||||
msgid "Authenticated Session"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:726
|
||||
#: authentik/core/models.py:739
|
||||
msgid "Authenticated Sessions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -585,65 +585,65 @@ msgstr ""
|
|||
msgid "Invalid kubeconfig"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:122
|
||||
#: authentik/outposts/models.py:121
|
||||
msgid ""
|
||||
"If enabled, use the local connection. Required Docker socket/Kubernetes "
|
||||
"Integration"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:152
|
||||
#: authentik/outposts/models.py:151
|
||||
msgid "Outpost Service-Connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:153
|
||||
#: authentik/outposts/models.py:152
|
||||
msgid "Outpost Service-Connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:161
|
||||
#: authentik/outposts/models.py:160
|
||||
msgid ""
|
||||
"Can be in the format of 'unix://<path>' when connecting to a local docker "
|
||||
"daemon, or 'https://<hostname>:2376' when connecting to a remote system."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:173
|
||||
#: authentik/outposts/models.py:172
|
||||
msgid ""
|
||||
"CA which the endpoint's Certificate is verified against. Can be left empty "
|
||||
"for no validation."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:185
|
||||
#: authentik/outposts/models.py:184
|
||||
msgid ""
|
||||
"Certificate/Key used for authentication. Can be left empty for no "
|
||||
"authentication."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:203
|
||||
#: authentik/outposts/models.py:202
|
||||
msgid "Docker Service-Connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:204
|
||||
#: authentik/outposts/models.py:203
|
||||
msgid "Docker Service-Connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:212
|
||||
#: authentik/outposts/models.py:211
|
||||
msgid ""
|
||||
"Paste your kubeconfig here. authentik will automatically use the currently "
|
||||
"selected context."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:218
|
||||
#: authentik/outposts/models.py:217
|
||||
msgid "Verify SSL Certificates of the Kubernetes API endpoint"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:235
|
||||
#: authentik/outposts/models.py:234
|
||||
msgid "Kubernetes Service-Connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:236
|
||||
#: authentik/outposts/models.py:235
|
||||
msgid "Kubernetes Service-Connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/outposts/models.py:252
|
||||
#: authentik/outposts/models.py:251
|
||||
msgid ""
|
||||
"Select Service-Connection authentik should use to manage this outpost. Leave "
|
||||
"empty if authentik should not handle the deployment."
|
||||
|
@ -1373,31 +1373,31 @@ msgstr ""
|
|||
msgid "SAML Property Mappings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:26
|
||||
#: authentik/providers/scim/models.py:20
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:27
|
||||
#: authentik/providers/scim/models.py:21
|
||||
msgid "Authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:33 authentik/sources/ldap/models.py:94
|
||||
#: authentik/providers/scim/models.py:27 authentik/sources/ldap/models.py:94
|
||||
msgid "Property mappings used for group creation/updating."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:75
|
||||
#: authentik/providers/scim/models.py:60
|
||||
msgid "SCIM Provider"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:76
|
||||
#: authentik/providers/scim/models.py:61
|
||||
msgid "SCIM Providers"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:96
|
||||
#: authentik/providers/scim/models.py:81
|
||||
msgid "SCIM Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:97
|
||||
#: authentik/providers/scim/models.py:82
|
||||
msgid "SCIM Mappings"
|
||||
msgstr ""
|
||||
|
||||
|
|
514
schema.yml
514
schema.yml
|
@ -4604,6 +4604,20 @@ paths:
|
|||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: type
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- default
|
||||
- external
|
||||
- internal_service_account
|
||||
- service_account
|
||||
description: |-
|
||||
* `default` - Default
|
||||
* `external` - External
|
||||
* `service_account` - Service Account
|
||||
* `internal_service_account` - Internal Service Account
|
||||
- in: query
|
||||
name: username
|
||||
schema:
|
||||
|
@ -4612,6 +4626,7 @@ paths:
|
|||
name: uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
|
@ -5527,6 +5542,356 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/license/:
|
||||
get:
|
||||
operationId: enterprise_license_list
|
||||
description: License Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- 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
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedLicenseList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: enterprise_license_create
|
||||
description: License Viewset
|
||||
tags:
|
||||
- enterprise
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LicenseRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/License'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/license/{license_uuid}/:
|
||||
get:
|
||||
operationId: enterprise_license_retrieve
|
||||
description: License Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: license_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this license.
|
||||
required: true
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/License'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: enterprise_license_update
|
||||
description: License Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: license_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this license.
|
||||
required: true
|
||||
tags:
|
||||
- enterprise
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LicenseRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/License'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: enterprise_license_partial_update
|
||||
description: License Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: license_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this license.
|
||||
required: true
|
||||
tags:
|
||||
- enterprise
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedLicenseRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/License'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: enterprise_license_destroy
|
||||
description: License Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: license_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this license.
|
||||
required: true
|
||||
tags:
|
||||
- enterprise
|
||||
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: ''
|
||||
/enterprise/license/{license_uuid}/used_by/:
|
||||
get:
|
||||
operationId: enterprise_license_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: license_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this license.
|
||||
required: true
|
||||
tags:
|
||||
- enterprise
|
||||
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: ''
|
||||
/enterprise/license/forecast/:
|
||||
get:
|
||||
operationId: enterprise_license_forecast_retrieve
|
||||
description: Forecast how many users will be required in a year
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LicenseForecast'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/license/get_install_id/:
|
||||
get:
|
||||
operationId: enterprise_license_get_install_id_retrieve
|
||||
description: Get install_id
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InstallID'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/enterprise/license/summary/:
|
||||
get:
|
||||
operationId: enterprise_license_summary_retrieve
|
||||
description: Get the total license status
|
||||
tags:
|
||||
- enterprise
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LicenseSummary'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/events/events/:
|
||||
get:
|
||||
operationId: events_events_list
|
||||
|
@ -30468,6 +30833,13 @@ components:
|
|||
type: boolean
|
||||
required:
|
||||
- name
|
||||
InstallID:
|
||||
type: object
|
||||
properties:
|
||||
install_id:
|
||||
type: string
|
||||
required:
|
||||
- install_id
|
||||
IntentEnum:
|
||||
enum:
|
||||
- verification
|
||||
|
@ -31336,6 +31708,86 @@ components:
|
|||
* `content_right` - CONTENT_RIGHT
|
||||
* `sidebar_left` - SIDEBAR_LEFT
|
||||
* `sidebar_right` - SIDEBAR_RIGHT
|
||||
License:
|
||||
type: object
|
||||
description: License Serializer
|
||||
properties:
|
||||
license_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
readOnly: true
|
||||
key:
|
||||
type: string
|
||||
expiry:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
users:
|
||||
type: integer
|
||||
readOnly: true
|
||||
external_users:
|
||||
type: integer
|
||||
readOnly: true
|
||||
required:
|
||||
- expiry
|
||||
- external_users
|
||||
- key
|
||||
- license_uuid
|
||||
- name
|
||||
- users
|
||||
LicenseForecast:
|
||||
type: object
|
||||
description: Serializer for license forecast
|
||||
properties:
|
||||
users:
|
||||
type: integer
|
||||
external_users:
|
||||
type: integer
|
||||
required:
|
||||
- external_users
|
||||
- users
|
||||
LicenseRequest:
|
||||
type: object
|
||||
description: License Serializer
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- key
|
||||
LicenseSummary:
|
||||
type: object
|
||||
description: Serializer for license status
|
||||
properties:
|
||||
users:
|
||||
type: integer
|
||||
external_users:
|
||||
type: integer
|
||||
valid:
|
||||
type: boolean
|
||||
show_admin_warning:
|
||||
type: boolean
|
||||
show_user_warning:
|
||||
type: boolean
|
||||
read_only:
|
||||
type: boolean
|
||||
latest_valid:
|
||||
type: string
|
||||
format: date-time
|
||||
has_license:
|
||||
type: boolean
|
||||
required:
|
||||
- external_users
|
||||
- has_license
|
||||
- latest_valid
|
||||
- read_only
|
||||
- show_admin_warning
|
||||
- show_user_warning
|
||||
- users
|
||||
- valid
|
||||
Link:
|
||||
type: object
|
||||
description: Returns a single link
|
||||
|
@ -33686,6 +34138,41 @@ components:
|
|||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedLicenseList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
type: object
|
||||
properties:
|
||||
next:
|
||||
type: number
|
||||
previous:
|
||||
type: number
|
||||
count:
|
||||
type: number
|
||||
current:
|
||||
type: number
|
||||
total_pages:
|
||||
type: number
|
||||
start_index:
|
||||
type: number
|
||||
end_index:
|
||||
type: number
|
||||
required:
|
||||
- next
|
||||
- previous
|
||||
- count
|
||||
- current
|
||||
- total_pages
|
||||
- start_index
|
||||
- end_index
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/License'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedNotificationList:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -36850,6 +37337,13 @@ components:
|
|||
type: string
|
||||
format: uuid
|
||||
description: Property mappings used for group creation/updating.
|
||||
PatchedLicenseRequest:
|
||||
type: object
|
||||
description: License Serializer
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedNotificationRequest:
|
||||
type: object
|
||||
description: Notification Serializer
|
||||
|
@ -38033,6 +38527,8 @@ components:
|
|||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
type:
|
||||
$ref: '#/components/schemas/UserTypeEnum'
|
||||
PatchedUserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
|
@ -41430,6 +41926,8 @@ components:
|
|||
readOnly: true
|
||||
path:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/UserTypeEnum'
|
||||
required:
|
||||
- avatar
|
||||
- groups_obj
|
||||
|
@ -41888,6 +42386,8 @@ components:
|
|||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
type:
|
||||
$ref: '#/components/schemas/UserTypeEnum'
|
||||
required:
|
||||
- name
|
||||
- username
|
||||
|
@ -41971,6 +42471,8 @@ components:
|
|||
additionalProperties: {}
|
||||
description: Get user settings with tenant and group settings applied
|
||||
readOnly: true
|
||||
type:
|
||||
$ref: '#/components/schemas/UserTypeEnum'
|
||||
required:
|
||||
- avatar
|
||||
- groups
|
||||
|
@ -42071,6 +42573,18 @@ components:
|
|||
- pk
|
||||
- source
|
||||
- user
|
||||
UserTypeEnum:
|
||||
enum:
|
||||
- default
|
||||
- external
|
||||
- service_account
|
||||
- internal_service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `default` - Default
|
||||
* `external` - External
|
||||
* `service_account` - Service Account
|
||||
* `internal_service_account` - Internal Service Account
|
||||
UserVerificationEnum:
|
||||
enum:
|
||||
- required
|
||||
|
|
|
@ -256,8 +256,6 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||
"homeDirectory": f"/home/{o_user.username}",
|
||||
"ak-active": True,
|
||||
"ak-superuser": False,
|
||||
"goauthentikio-user-override-ips": True,
|
||||
"goauthentikio-user-service-account": True,
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
|
@ -284,8 +282,6 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||
"homeDirectory": f"/home/{embedded_account.username}",
|
||||
"ak-active": True,
|
||||
"ak-superuser": False,
|
||||
"goauthentikio-user-override-ips": True,
|
||||
"goauthentikio-user-service-account": True,
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
|
|
|
@ -8,3 +8,4 @@ coverage
|
|||
poly.ts
|
||||
src/locale-codes.ts
|
||||
src/locales/
|
||||
storybook-static/
|
||||
|
|
|
@ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users";
|
|||
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||
import { Interface } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/ak-locale-context";
|
||||
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@goauthentik/elements/notifications/APIDrawer";
|
||||
|
@ -30,7 +31,14 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
|||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { AdminApi, CoreApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api";
|
||||
import {
|
||||
AdminApi,
|
||||
CapabilitiesEnum,
|
||||
CoreApi,
|
||||
SessionUser,
|
||||
UiThemeEnum,
|
||||
Version,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-interface-admin")
|
||||
export class AdminInterface extends Interface {
|
||||
|
@ -67,7 +75,17 @@ export class AdminInterface extends Interface {
|
|||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
ak-locale-context {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.pf-c-page {
|
||||
flex-grow: 1;
|
||||
background-color: var(--pf-c-page--BackgroundColor) !important;
|
||||
}
|
||||
/* Global page background colour */
|
||||
|
@ -113,7 +131,8 @@ export class AdminInterface extends Interface {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html` <ak-locale-context
|
||||
><div class="pf-c-page">
|
||||
><ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||
<div class="pf-c-page">
|
||||
<ak-sidebar
|
||||
class="pf-c-page__sidebar ${this.sidebarOpen
|
||||
? "pf-m-expanded"
|
||||
|
@ -308,6 +327,16 @@ export class AdminInterface extends Interface {
|
|||
<span slot="label">${msg("Outpost Integrations")}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
${this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
|
||||
? html`
|
||||
<ak-sidebar-item>
|
||||
<span slot="label">${msg("Enterprise")}</span>
|
||||
<ak-sidebar-item path="/enterprise/licenses">
|
||||
<span slot="label">${msg("Licenses")}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
`
|
||||
: html``}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,4 +136,8 @@ export const ROUTES: Route[] = [
|
|||
await import("@goauthentik/admin/DebugPage");
|
||||
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
|
||||
}),
|
||||
new Route(new RegExp("^/enterprise/licenses$"), async () => {
|
||||
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
|
||||
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -26,6 +26,7 @@ export class SystemStatusCard extends AdminStatusCard<System> {
|
|||
// First install, ensure the embedded outpost host is set
|
||||
// also run when outpost host does not contain http
|
||||
// (yes it's called host and requires a URL, i know)
|
||||
// TODO: Improve this in OOB flow
|
||||
await this.setOutpostHost();
|
||||
status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { EnterpriseApi, License } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-enterprise-license-form")
|
||||
export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
||||
@state()
|
||||
installID?: string;
|
||||
|
||||
loadInstance(pk: string): Promise<License> {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseRetrieve({
|
||||
licenseUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return msg("Successfully updated license.");
|
||||
} else {
|
||||
return msg("Successfully created license.");
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.installID = (
|
||||
await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseGetInstallIdRetrieve()
|
||||
).installId;
|
||||
}
|
||||
|
||||
async send(data: License): Promise<License> {
|
||||
if (this.instance) {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({
|
||||
licenseUuid: this.instance.licenseUuid || "",
|
||||
patchedLicenseRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({
|
||||
licenseRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Install ID")}>
|
||||
<input class="pf-c-form-control" readonly type="text" value="${this.installID}" />
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="key"
|
||||
?writeOnly=${this.instance !== undefined}
|
||||
label=${msg("License key")}
|
||||
>
|
||||
<textarea class="pf-c-form-control"></textarea>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
import "@goauthentik/admin/enterprise/EnterpriseLicenseForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
import "@goauthentik/elements/Spinner";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/cards/AggregateCard";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
|
||||
import { EnterpriseApi, License, LicenseForecast, LicenseSummary } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-enterprise-license-list")
|
||||
export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||
checkbox = true;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
pageTitle(): string {
|
||||
return msg("Licenses");
|
||||
}
|
||||
pageDescription(): string {
|
||||
return msg("Manage enterprise licenses");
|
||||
}
|
||||
pageIcon(): string {
|
||||
return "pf-icon pf-icon-key";
|
||||
}
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
|
||||
@state()
|
||||
forecast?: LicenseForecast;
|
||||
|
||||
@state()
|
||||
summary?: LicenseSummary;
|
||||
|
||||
@state()
|
||||
installID?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(
|
||||
PFDescriptionList,
|
||||
PFGrid,
|
||||
PFBanner,
|
||||
PFFormControl,
|
||||
PFButton,
|
||||
PFCard,
|
||||
css`
|
||||
.pf-m-no-padding-bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<License>> {
|
||||
this.forecast = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseForecastRetrieve();
|
||||
this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve();
|
||||
this.installID = (
|
||||
await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseGetInstallIdRetrieve()
|
||||
).installId;
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(msg("Name"), "name"),
|
||||
new TableColumn(msg("Users")),
|
||||
new TableColumn(msg("Expiry date")),
|
||||
new TableColumn(msg("Actions")),
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("License(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: License) => {
|
||||
return [
|
||||
{ key: msg("Name"), value: item.name },
|
||||
{ key: msg("Expiry"), value: item.expiry?.toLocaleString() },
|
||||
];
|
||||
}}
|
||||
.usedBy=${(item: License) => {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseUsedByList({
|
||||
licenseUuid: item.licenseUuid,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: License) => {
|
||||
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseDestroy({
|
||||
licenseUuid: item.licenseUuid,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
renderSectionBefore(): TemplateResult {
|
||||
return html`
|
||||
<div class="pf-c-banner pf-m-info">
|
||||
${msg("Enterprise is in preview.")}
|
||||
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||
</div>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-bottom">
|
||||
<div
|
||||
class="pf-l-grid pf-m-gutter pf-m-all-6-col-on-sm pf-m-all-4-col-on-md pf-m-all-3-col-on-lg pf-m-all-3-col-on-xl"
|
||||
>
|
||||
<div class="pf-l-grid__item pf-c-card">
|
||||
<div class="pf-c-card__title">${msg("How to get a license")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.installID
|
||||
? html` <a
|
||||
target="_blank"
|
||||
href=${`https://customers.goauthentik.io/from_authentik/purchase/?install_id=${this.installID}`}
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>${msg("Go to the customer portal")}</a
|
||||
>`
|
||||
: html`<ak-spinner></ak-spinner>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-c-card">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-user"
|
||||
header=${msg("Forecasted default users")}
|
||||
subtext=${msg("Estimated user count one year from now")}
|
||||
>
|
||||
${this.forecast?.users}
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-c-card">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-user"
|
||||
header=${msg("Forecasted external users")}
|
||||
subtext=${msg("Estimated external user count one year from now")}
|
||||
>
|
||||
${this.forecast?.externalUsers}
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-c-card">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-user"
|
||||
header=${msg("Expiry")}
|
||||
subtext=${msg("Cumulative license expiry")}
|
||||
>
|
||||
${this.summary?.hasLicense
|
||||
? this.summary.latestValid.toLocaleString()
|
||||
: "-"}
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
row(item: License): TemplateResult[] {
|
||||
let color = PFColor.Green;
|
||||
if (item.expiry) {
|
||||
const now = new Date();
|
||||
const inAMonth = new Date();
|
||||
inAMonth.setDate(inAMonth.getDate() + 30);
|
||||
if (item.expiry <= inAMonth) {
|
||||
color = PFColor.Orange;
|
||||
}
|
||||
if (item.expiry <= now) {
|
||||
color = PFColor.Red;
|
||||
}
|
||||
}
|
||||
return [
|
||||
html`<div>${item.name}</div>`,
|
||||
html`<div>
|
||||
<small>0 / ${item.users}</small>
|
||||
<small>0 / ${item.externalUsers}</small>
|
||||
</div>`,
|
||||
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update License")} </span>
|
||||
<ak-enterprise-license-form slot="form" .instancePk=${item.licenseUuid}>
|
||||
</ak-enterprise-license-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
<span slot="header"> ${msg("Create License")} </span>
|
||||
<ak-enterprise-license-form slot="form"> </ak-enterprise-license-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import "@goauthentik/admin/users/GroupSelectModal";
|
||||
import { UserTypeEnum } from "@goauthentik/api/dist/models/UserTypeEnum";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
@ -75,6 +77,31 @@ export class UserForm extends ModelForm<User, number> {
|
|||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("User's display name.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("User type")} ?required=${true} name="type">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
// TODO: Add better copy
|
||||
{
|
||||
label: "Default",
|
||||
value: UserTypeEnum.Default,
|
||||
default: true,
|
||||
description: html`${msg("Default user")}`,
|
||||
},
|
||||
{
|
||||
label: "External",
|
||||
value: UserTypeEnum.External,
|
||||
description: html`${msg("External user")}`,
|
||||
},
|
||||
{
|
||||
label: "Service account",
|
||||
value: UserTypeEnum.ServiceAccount,
|
||||
description: html`${msg("Service account")}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.type}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Email")} name="email">
|
||||
<input
|
||||
type="email"
|
||||
|
|
|
@ -19,6 +19,9 @@ export class AggregateCard extends AKElement {
|
|||
@property()
|
||||
headerLink?: string;
|
||||
|
||||
@property()
|
||||
subtext?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
isCenter = true;
|
||||
|
||||
|
@ -79,6 +82,7 @@ export class AggregateCard extends AKElement {
|
|||
</div>
|
||||
<div class="pf-c-card__body ${this.isCenter ? "center-value" : ""}">
|
||||
${this.renderInner()}
|
||||
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : html``}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
|
||||
import { EnterpriseApi, LicenseSummary } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-enterprise-status")
|
||||
export class EnterpriseStatusBanner extends AKElement {
|
||||
@state()
|
||||
summary?: LicenseSummary;
|
||||
|
||||
@property()
|
||||
interface: "admin" | "user" | "" = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBanner];
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((b) => {
|
||||
this.summary = b;
|
||||
});
|
||||
}
|
||||
|
||||
renderBanner(): TemplateResult {
|
||||
return html`<div class="pf-c-banner ${this.summary?.readOnly ? "pf-m-red" : "pf-m-orange"}">
|
||||
${msg("Warning: The current user count has exceeded the configured licenses.")}
|
||||
<a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
switch (this.interface.toLowerCase()) {
|
||||
case "admin":
|
||||
if (this.summary?.showAdminWarning || this.summary?.readOnly) {
|
||||
return this.renderBanner();
|
||||
}
|
||||
break;
|
||||
case "user":
|
||||
if (this.summary?.showUserWarning || this.summary?.readOnly) {
|
||||
return this.renderBanner();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,16 @@ export abstract class TablePage<T> extends Table<T> {
|
|||
return html``;
|
||||
}
|
||||
|
||||
// Optionally render section above the table
|
||||
renderSectionBefore(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Optionally render section below the table
|
||||
renderSectionAfter(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderEmpty(inner?: TemplateResult): TemplateResult {
|
||||
return super.renderEmpty(html`
|
||||
${inner
|
||||
|
@ -75,6 +85,7 @@ export abstract class TablePage<T> extends Table<T> {
|
|||
description=${ifDefined(this.pageDescription())}
|
||||
>
|
||||
</ak-page-header>
|
||||
${this.renderSectionBefore()}
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-sidebar pf-m-gutter">
|
||||
<div class="pf-c-sidebar__main">
|
||||
|
@ -85,6 +96,7 @@ export abstract class TablePage<T> extends Table<T> {
|
|||
${this.renderSidebarAfter()}
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
</section>
|
||||
${this.renderSectionAfter()}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import { first } from "@goauthentik/common/utils";
|
|||
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||
import { Interface } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/ak-locale-context";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@goauthentik/elements/notifications/APIDrawer";
|
||||
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||
|
@ -35,7 +37,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||
|
||||
import { EventsApi, SessionUser } from "@goauthentik/api";
|
||||
import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-interface-user")
|
||||
export class UserInterface extends Interface {
|
||||
|
@ -148,6 +150,7 @@ export class UserInterface extends Interface {
|
|||
userDisplay = this.me.user.username;
|
||||
}
|
||||
return html` <ak-locale-context>
|
||||
<ak-enterprise-status interface="user"></ak-enterprise-status>
|
||||
<div class="pf-c-page">
|
||||
<div class="background-wrapper" style="${this.uiConfig.theme.background}"></div>
|
||||
<header class="pf-c-page__header">
|
||||
|
@ -243,18 +246,23 @@ export class UserInterface extends Interface {
|
|||
: html``}
|
||||
</div>
|
||||
${this.me.original
|
||||
? html`<div class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<a
|
||||
class="pf-c-button pf-m-warning pf-m-small"
|
||||
href=${`/-/impersonation/end/?back=${encodeURIComponent(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`}
|
||||
>
|
||||
${msg("Stop impersonation")}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
? html`
|
||||
<div class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<ak-action-button
|
||||
class="pf-m-warning pf-m-small"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersImpersonateEndRetrieve()
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
>
|
||||
${msg("Stop impersonation")}
|
||||
</ak-action-button>
|
||||
</div>
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<div
|
||||
|
|
|
@ -5747,6 +5747,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -6063,6 +6063,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5655,6 +5655,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5762,6 +5762,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5894,6 +5894,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5998,6 +5998,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5645,6 +5645,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -7580,6 +7580,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
<target>启用时,可以通过在密码后添加分号和 TOTP 代码来使用基于代码的多因素身份验证。仅在所有绑定到此提供程序的用户都已配置 TOTP 设备的情况下才应该启用,否则密码可能会因为包含分号而被错误地拒绝。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5700,6 +5700,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5699,6 +5699,84 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s1889ba2eaeec2f1e">
|
||||
<source>When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f9492d30a96b9c6">
|
||||
<source>User type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b9a40b7b2853c7d">
|
||||
<source>Default user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35b9fa270f45b391">
|
||||
<source>External user</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1a635369edaf4dc3">
|
||||
<source>Service account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0e427111d750cc02">
|
||||
<source>Successfully updated license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s06ae64e621f302eb">
|
||||
<source>Successfully created license.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2905c425adae99bd">
|
||||
<source>Install ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb18ec434a8a3aafb">
|
||||
<source>License key</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e109263b73c12d5">
|
||||
<source>Licenses</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf8f9f3032e891e16">
|
||||
<source>TODO Copy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd49099e9522635f4">
|
||||
<source>License(s)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3be1d90ffa46b7f1">
|
||||
<source>Enterprise is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s34dca481f039c226">
|
||||
<source>How to get a license</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s948364901c166232">
|
||||
<source>Copy the installation ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s75c167446b237e0f">
|
||||
<source>Then open the customer portal</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9748dd3bd53d27a4">
|
||||
<source>Forecasted default users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b18f594d94c2374">
|
||||
<source>Estimated user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s69f246d164be88d0">
|
||||
<source>Forecasted external users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s878fc2eaf94642db">
|
||||
<source>Estimated external user count one year from now</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd22bd01bdf28c548">
|
||||
<source>Cumulative license expiry</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb6cee42435dd07">
|
||||
<source>Update License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s99afa741c259d70e">
|
||||
<source>Create License</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7df5b92a3f93544f">
|
||||
<source>Warning: The current user count has exceeded the configured licenses.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0141f42936495787">
|
||||
<source>Click here for more info.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7be2df39f727faa2">
|
||||
<source>Enterprise</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ce7cc01fb9b5b53">
|
||||
<source>Manage enterprise licenses</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
Reference in New Issue