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:
Jens L 2023-07-17 17:57:08 +02:00 committed by GitHub
parent cf799fca03
commit 41af486006
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 2534 additions and 128 deletions

1
.gitignore vendored
View File

@ -204,3 +204,4 @@ data/
# Local Netlify folder
.netlify
.ruff_cache

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

150
authentik/enterprise/api.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

@ -0,0 +1,7 @@
"""API URLs"""
from authentik.enterprise.api import LicenseViewSet
api_urlpatterns = [
("enterprise/license", LicenseViewSet),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,3 +8,4 @@ coverage
poly.ts
src/locale-codes.ts
src/locales/
storybook-static/

View File

@ -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``}
`;
}
}

View File

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

View File

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

View File

@ -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>`;
}
}

View File

@ -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>
`;
}
}

View File

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

View File

@ -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>`;
}

View File

@ -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``;
}
}

View File

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

View File

@ -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,16 +246,21 @@ export class UserInterface extends Interface {
: html``}
</div>
${this.me.original
? html`<div class="pf-c-page__header-tools">
? html`&nbsp;
<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}`,
)}`}
<ak-action-button
class="pf-m-warning pf-m-small"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateEndRetrieve()
.then(() => {
window.location.reload();
});
}}
>
${msg("Stop impersonation")}
</a>
</ak-action-button>
</div>
</div>`
: html``}

View File

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

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

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

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

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

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

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

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

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

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