Compare commits

..

1 Commits

Author SHA1 Message Date
115973376f Updated hackathon page
Added exact UTC times, info about Discord, emotes, and "legalese" about prize money

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-07-20 18:34:29 -05:00
136 changed files with 2965 additions and 5470 deletions

View File

@ -1,39 +0,0 @@
# Rename transifex pull requests to have a correct naming
name: authentik-translation-transifex-rename
on:
pull_request:
types: [opened, reopened]
jobs:
rename_pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Get current title
id: title
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
title=$(curl -q -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }} | jq -r .title)
echo "title=${title}" >> "$GITHUB_OUTPUT"
- name: Rename
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
curl -L \
-X PATCH \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }} \
-d "{\"title\":\"translate: ${{ steps.title.outputs.title }}\"}"

View File

@ -21,14 +21,9 @@ _other_urls = []
for _authentik_app in get_apps():
try:
api_urls = import_module(f"{_authentik_app.name}.urls")
except (ModuleNotFoundError, ImportError) as exc:
LOGGER.warning("Could not import app's URLs", app_name=_authentik_app.name, exc=exc)
except (ModuleNotFoundError, ImportError):
continue
if not hasattr(api_urls, "api_urlpatterns"):
LOGGER.debug(
"App does not define API URLs",
app_name=_authentik_app.name,
)
continue
urls: list = getattr(api_urls, "api_urlpatterns")
for url in urls:

View File

@ -12,7 +12,7 @@ def migrate_user_type(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.core.models import UserTypes
for user in User.objects.using(db_alias).all():
user.type = UserTypes.INTERNAL
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:

View File

@ -1,41 +0,0 @@
# Generated by Django 4.1.10 on 2023-07-21 12:54
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_user_type_v2(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():
if user.type != "default":
continue
user.type = UserTypes.INTERNAL
user.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0030_user_type"),
]
operations = [
migrations.AlterField(
model_name="user",
name="type",
field=models.TextField(
choices=[
("internal", "Internal"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
],
default="internal",
),
),
migrations.RunPython(migrate_user_type_v2),
]

View File

@ -67,7 +67,7 @@ class UserTypes(models.TextChoices):
"""User types, both for grouping, licensing and permissions in the case
of the internal_service_account"""
INTERNAL = "internal"
DEFAULT = "default"
EXTERNAL = "external"
# User-created service accounts
@ -161,7 +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.INTERNAL)
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

@ -137,7 +137,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
last_month = now() - timedelta(days=30)
# Forecast for default users
users_in_last_month = User.objects.filter(
type=UserTypes.INTERNAL, date_joined__gte=last_month
type=UserTypes.DEFAULT, date_joined__gte=last_month
).count()
# Forecast for external users
external_in_last_month = LicenseKey.get_external_user_count()

View File

@ -9,7 +9,7 @@ from time import mktime
from uuid import uuid4
from cryptography.exceptions import InvalidSignature
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
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
@ -61,8 +61,8 @@ class LicenseKey:
if len(x5c) < 1:
raise ValidationError("Unable to verify license")
try:
our_cert = load_der_x509_certificate(b64decode(x5c[0]))
intermediate = load_der_x509_certificate(b64decode(x5c[1]))
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):
@ -73,7 +73,7 @@ class LicenseKey:
decode(
jwt,
our_cert.public_key(),
algorithms=["ES512"],
algorithms=["ES521"],
audience=get_license_aud(),
),
)
@ -105,7 +105,7 @@ class LicenseKey:
@staticmethod
def get_default_user_count():
"""Get current default user count"""
return LicenseKey.base_user_qs().filter(type=UserTypes.INTERNAL).count()
return LicenseKey.base_user_qs().filter(type=UserTypes.DEFAULT).count()
@staticmethod
def get_external_user_count():

View File

@ -24,7 +24,7 @@ class EnterprisePolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult:
if not LicenseKey.get_total().is_valid():
return PolicyResult(False)
if request.user.type != UserTypes.INTERNAL:
if request.user.type != UserTypes.DEFAULT:
return PolicyResult(False)
return PolicyResult(True)

View File

@ -76,20 +76,9 @@ class TaskInfo:
return cache.get_many(cache.keys(CACHE_KEY_PREFIX + name)).values()
return cache.get(CACHE_KEY_PREFIX + name, None)
@property
def full_name(self) -> str:
"""Get the full cache key with task name and UID"""
key = CACHE_KEY_PREFIX + self.task_name
if self.result.uid:
uid_suffix = f":{self.result.uid}"
key += uid_suffix
if not self.task_name.endswith(uid_suffix):
self.task_name += uid_suffix
return key
def delete(self):
"""Delete task info from cache"""
return cache.delete(self.full_name)
return cache.delete(CACHE_KEY_PREFIX + self.task_name)
def update_metrics(self):
"""Update prometheus metrics"""
@ -108,8 +97,12 @@ class TaskInfo:
def save(self, timeout_hours=6):
"""Save task into cache"""
key = CACHE_KEY_PREFIX + self.task_name
if self.result.uid:
key += f":{self.result.uid}"
self.task_name += f":{self.result.uid}"
self.update_metrics()
cache.set(self.full_name, self, timeout=timeout_hours * 60 * 60)
cache.set(key, self, timeout=timeout_hours * 60 * 60)
class MonitoredTask(Task):

View File

@ -1,43 +0,0 @@
"""Test Monitored tasks"""
from django.test import TestCase
from authentik.events.monitored_tasks import MonitoredTask, TaskInfo, TaskResult, TaskResultStatus
from authentik.lib.generators import generate_id
from authentik.root.celery import CELERY_APP
class TestMonitoredTasks(TestCase):
"""Test Monitored tasks"""
def test_failed_successful_remove_state(self):
"""Test that a task with `save_on_success` set to `False` that failed saves
a state, and upon successful completion will delete the state"""
should_fail = True
uid = generate_id()
@CELERY_APP.task(
bind=True,
base=MonitoredTask,
)
def test_task(self: MonitoredTask):
self.save_on_success = False
self.set_uid(uid)
self.set_status(
TaskResult(TaskResultStatus.ERROR if should_fail else TaskResultStatus.SUCCESSFUL)
)
# First test successful run
should_fail = False
test_task.delay().get()
self.assertIsNone(TaskInfo.by_name(f"test_task:{uid}"))
# Then test failed
should_fail = True
test_task.delay().get()
info = TaskInfo.by_name(f"test_task:{uid}")
self.assertEqual(info.result.status, TaskResultStatus.ERROR)
# Then after that, the state should be removed
should_fail = False
test_task.delay().get()
self.assertIsNone(TaskInfo.by_name(f"test_task:{uid}"))

10
authentik/lib/apps.py Normal file
View File

@ -0,0 +1,10 @@
"""authentik lib app config"""
from django.apps import AppConfig
class AuthentikLibConfig(AppConfig):
"""authentik lib app config"""
name = "authentik.lib"
label = "authentik_lib"
verbose_name = "authentik lib"

View File

@ -1,22 +1,16 @@
"""Base Kubernetes Reconciler"""
from dataclasses import asdict
from json import dumps
from typing import TYPE_CHECKING, Generic, Optional, TypeVar
from dacite.core import from_dict
from django.utils.text import slugify
from jsonpatch import JsonPatchConflict, JsonPatchException, JsonPatchTestFailed, apply_patch
from kubernetes.client import ApiClient, V1ObjectMeta
from kubernetes.client import V1ObjectMeta
from kubernetes.client.exceptions import ApiException, OpenApiException
from kubernetes.client.models.v1_deployment import V1Deployment
from kubernetes.client.models.v1_pod import V1Pod
from requests import Response
from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
if TYPE_CHECKING:
@ -40,23 +34,11 @@ class KubernetesObjectReconciler(Generic[T]):
self.namespace = controller.outpost.config.kubernetes_namespace
self.logger = get_logger().bind(type=self.__class__.__name__)
def get_patch(self):
"""Get any patches that apply to this CRD"""
patches = self.controller.outpost.config.kubernetes_json_patches
if not patches:
return None
return patches.get(self.reconciler_name(), None)
@property
def is_embedded(self) -> bool:
"""Return true if the current outpost is embedded"""
return self.controller.outpost.managed == MANAGED_OUTPOST
@staticmethod
def reconciler_name() -> str:
"""A name this reconciler is identified by in the configuration"""
raise NotImplementedError
@property
def noop(self) -> bool:
"""Return true if this object should not be created/updated/deleted in this cluster"""
@ -73,32 +55,6 @@ class KubernetesObjectReconciler(Generic[T]):
}
).lower()
def get_patched_reference_object(self) -> T:
"""Get patched reference object"""
reference = self.get_reference_object()
patch = self.get_patch()
try:
json = ApiClient().sanitize_for_serialization(reference)
# Custom objects will not be known to the clients openapi types
except AttributeError:
json = asdict(reference)
try:
ref = json
if patch is not None:
ref = apply_patch(json, patch)
except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
raise ControllerException(f"JSON Patch failed: {exc}") from exc
mock_response = Response()
mock_response.data = dumps(ref)
try:
result = ApiClient().deserialize(mock_response, reference.__class__.__name__)
# Custom objects will not be known to the clients openapi types
except AttributeError:
result = from_dict(reference.__class__, data=ref)
return result
# pylint: disable=invalid-name
def up(self):
"""Create object if it doesn't exist, update if needed or recreate if needed."""
@ -106,7 +62,7 @@ class KubernetesObjectReconciler(Generic[T]):
if self.noop:
self.logger.debug("Object is noop")
return
reference = self.get_patched_reference_object()
reference = self.get_reference_object()
try:
try:
current = self.retrieve()
@ -173,16 +129,6 @@ class KubernetesObjectReconciler(Generic[T]):
if current.metadata.labels != reference.metadata.labels:
raise NeedsUpdate()
patch = self.get_patch()
if patch is not None:
current_json = ApiClient().sanitize_for_serialization(current)
try:
if apply_patch(current_json, patch) != current_json:
raise NeedsUpdate()
except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
raise ControllerException(f"JSON Patch failed: {exc}") from exc
def create(self, reference: T):
"""API Wrapper to create object"""
raise NotImplementedError

View File

@ -43,10 +43,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
self.api = AppsV1Api(controller.client)
self.outpost = self.controller.outpost
@staticmethod
def reconciler_name() -> str:
return "deployment"
def reconcile(self, current: V1Deployment, reference: V1Deployment):
compare_ports(
current.spec.template.spec.containers[0].ports,

View File

@ -24,10 +24,6 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
super().__init__(controller)
self.api = CoreV1Api(controller.client)
@staticmethod
def reconciler_name() -> str:
return "secret"
def reconcile(self, current: V1Secret, reference: V1Secret):
super().reconcile(current, reference)
for key in reference.data.keys():

View File

@ -20,10 +20,6 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
super().__init__(controller)
self.api = CoreV1Api(controller.client)
@staticmethod
def reconciler_name() -> str:
return "service"
def reconcile(self, current: V1Service, reference: V1Service):
compare_ports(current.spec.ports, reference.spec.ports)
# run the base reconcile last, as that will probably raise NeedsUpdate

View File

@ -71,10 +71,6 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
self.api_ex = ApiextensionsV1Api(controller.client)
self.api = CustomObjectsApi(controller.client)
@staticmethod
def reconciler_name() -> str:
return "prometheus servicemonitor"
@property
def noop(self) -> bool:
return (not self._crd_exists()) or (self.is_embedded)

View File

@ -64,19 +64,12 @@ class KubernetesController(BaseController):
super().__init__(outpost, connection)
self.client = KubernetesClient(connection)
self.reconcilers = {
SecretReconciler.reconciler_name(): SecretReconciler,
DeploymentReconciler.reconciler_name(): DeploymentReconciler,
ServiceReconciler.reconciler_name(): ServiceReconciler,
PrometheusServiceMonitorReconciler.reconciler_name(): (
PrometheusServiceMonitorReconciler
),
"secret": SecretReconciler,
"deployment": DeploymentReconciler,
"service": ServiceReconciler,
"prometheus servicemonitor": PrometheusServiceMonitorReconciler,
}
self.reconcile_order = [
SecretReconciler.reconciler_name(),
DeploymentReconciler.reconciler_name(),
ServiceReconciler.reconciler_name(),
PrometheusServiceMonitorReconciler.reconciler_name(),
]
self.reconcile_order = ["secret", "deployment", "service", "prometheus servicemonitor"]
def up(self):
try:

View File

@ -1,7 +1,7 @@
"""Outpost models"""
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any, Iterable, Optional
from typing import Iterable, Optional
from uuid import uuid4
from dacite.core import from_dict
@ -75,7 +75,6 @@ class OutpostConfig:
kubernetes_service_type: str = field(default="ClusterIP")
kubernetes_disabled_components: list[str] = field(default_factory=list)
kubernetes_image_pull_secrets: list[str] = field(default_factory=list)
kubernetes_json_patches: Optional[dict[str, list[dict[str, Any]]]] = field(default=None)
class OutpostModel(Model):

View File

@ -11,7 +11,6 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_cert,
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS,
GRANT_TYPE_PASSWORD,
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
@ -151,28 +150,3 @@ class TestTokenClientCredentials(OAuthTestCase):
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_password(self):
"""test successful (password grant)"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_PASSWORD,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)

View File

@ -459,13 +459,13 @@ class TokenView(View):
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
LOGGER.debug("Refreshing refresh token")
return TokenResponse(self.create_refresh_response())
if self.params.grant_type in [GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_PASSWORD]:
LOGGER.debug("Client credentials/password grant")
if self.params.grant_type == GRANT_TYPE_CLIENT_CREDENTIALS:
LOGGER.debug("Client credentials grant")
return TokenResponse(self.create_client_credentials_response())
if self.params.grant_type == GRANT_TYPE_DEVICE_CODE:
LOGGER.debug("Device code grant")
return TokenResponse(self.create_device_code_response())
raise TokenError("unsupported_grant_type")
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except (TokenError, DeviceCodeError) as error:
return TokenResponse(error.create_dict(), status=400)
except UserAuthError as error:

View File

@ -31,10 +31,6 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
super().__init__(controller)
self.api = NetworkingV1Api(controller.client)
@staticmethod
def reconciler_name() -> str:
return "ingress"
def _check_annotations(self, reference: V1Ingress):
"""Check that all annotations *we* set are correct"""
for key, value in self.get_ingress_annotations().items():

View File

@ -17,28 +17,24 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler):
if not self.reconciler.crd_exists():
self.reconciler = Traefik2MiddlewareReconciler(controller)
@staticmethod
def reconciler_name() -> str:
return "traefik middleware"
@property
def noop(self) -> bool:
return self.reconciler.noop
def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.reconciler.reconcile(current, reference)
return self.reconcile(current, reference)
def get_reference_object(self) -> TraefikMiddleware:
return self.reconciler.get_reference_object()
return self.get_reference_object()
def create(self, reference: TraefikMiddleware):
return self.reconciler.create(reference)
return self.create(reference)
def delete(self, reference: TraefikMiddleware):
return self.reconciler.delete(reference)
return self.delete(reference)
def retrieve(self) -> TraefikMiddleware:
return self.reconciler.retrieve()
return self.retrieve()
def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.reconciler.update(current, reference)
return self.update(current, reference)

View File

@ -67,10 +67,6 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
self.crd_version = "v1alpha1"
self.crd_plural = "middlewares"
@staticmethod
def reconciler_name() -> str:
return "traefik middleware"
@property
def noop(self) -> bool:
if not ProxyProvider.objects.filter(

View File

@ -16,9 +16,7 @@ class ProxyKubernetesController(KubernetesController):
DeploymentPort(9300, "http-metrics", "tcp"),
DeploymentPort(9443, "https", "tcp"),
]
self.reconcilers[IngressReconciler.reconciler_name()] = IngressReconciler
self.reconcilers[
TraefikMiddlewareReconciler.reconciler_name()
] = TraefikMiddlewareReconciler
self.reconcile_order.append(IngressReconciler.reconciler_name())
self.reconcile_order.append(TraefikMiddlewareReconciler.reconciler_name())
self.reconcilers["ingress"] = IngressReconciler
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
self.reconcile_order.append("ingress")
self.reconcile_order.append("traefik middleware")

View File

@ -10,8 +10,6 @@ from django.contrib.sessions.exceptions import SessionInterrupted
from django.contrib.sessions.middleware import SessionMiddleware as UpstreamSessionMiddleware
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.middleware.csrf import CSRF_SESSION_KEY
from django.middleware.csrf import CsrfViewMiddleware as UpstreamCsrfViewMiddleware
from django.utils.cache import patch_vary_headers
from django.utils.http import http_date
from jwt import PyJWTError, decode, encode
@ -133,29 +131,6 @@ class SessionMiddleware(UpstreamSessionMiddleware):
return response
class CsrfViewMiddleware(UpstreamCsrfViewMiddleware):
"""Dynamically set secure depending if the upstream connection is TLS or not"""
def _set_csrf_cookie(self, request: HttpRequest, response: HttpResponse):
if settings.CSRF_USE_SESSIONS:
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
else:
secure = SessionMiddleware.is_secure(request)
response.set_cookie(
settings.CSRF_COOKIE_NAME,
request.META["CSRF_COOKIE"],
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=secure,
httponly=settings.CSRF_COOKIE_HTTPONLY,
samesite=settings.CSRF_COOKIE_SAMESITE,
)
# Set the Vary header since content varies with the CSRF cookie.
patch_vary_headers(response, ("Cookie",))
class ChannelsLoggingMiddleware:
"""Logging middleware for channels"""

View File

@ -66,6 +66,7 @@ INSTALLED_APPS = [
"authentik.crypto",
"authentik.events",
"authentik.flows",
"authentik.lib",
"authentik.outposts",
"authentik.policies.dummy",
"authentik.policies.event_matcher",
@ -225,7 +226,7 @@ MIDDLEWARE = [
"authentik.events.middleware.AuditMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"authentik.root.middleware.CsrfViewMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"authentik.core.middleware.ImpersonateMiddleware",

View File

@ -3,10 +3,7 @@ from django.core.management.base import BaseCommand
from structlog.stdlib import get_logger
from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tasks import ldap_sync_paginator
from authentik.sources.ldap.tasks import ldap_sync_single
LOGGER = get_logger()
@ -23,10 +20,4 @@ class Command(BaseCommand):
if not source:
LOGGER.warning("Source does not exist", slug=source_slug)
continue
tasks = (
ldap_sync_paginator(source, UserLDAPSynchronizer)
+ ldap_sync_paginator(source, GroupLDAPSynchronizer)
+ ldap_sync_paginator(source, MembershipLDAPSynchronizer)
)
for task in tasks:
task()
ldap_sync_single(source)

View File

@ -49,7 +49,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
uniq = self._flatten(attributes[self._source.object_uniqueness_field])
try:
defaults = self.build_user_properties(user_dn, **attributes)
self._logger.debug("Writing user with attributes", **defaults)
self._logger.debug("Creating user with attributes", **defaults)
if "username" not in defaults:
raise IntegrityError("Username was not set by propertymappings")
ak_user, created = self.update_or_create_attributes(

View File

@ -59,7 +59,7 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) ->
signatures = []
for page in sync_inst.get_objects():
page_cache_key = CACHE_KEY_PREFIX + str(uuid4())
cache.set(page_cache_key, page, 60 * 60 * int(CONFIG.get("ldap.task_timeout_hours")))
cache.set(page_cache_key, page)
page_sync = ldap_sync.si(source.pk, class_to_path(sync), page_cache_key)
signatures.append(page_sync)
return signatures
@ -86,12 +86,6 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_k
sync_inst: BaseLDAPSynchronizer = sync(source)
page = cache.get(page_cache_key)
if not page:
error_message = (
f"Could not find page in cache: {page_cache_key}. "
+ "Try increasing ldap.task_timeout_hours"
)
LOGGER.warning(error_message)
self.set_status(TaskResult(TaskResultStatus.ERROR, [error_message]))
return
cache.touch(page_cache_key)
count = sync_inst.sync(page)

View File

@ -8,14 +8,12 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
from authentik.lib.generators import generate_key
from authentik.lib.utils.reflection import class_to_path
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tasks import ldap_sync, ldap_sync_all
from authentik.sources.ldap.tasks import ldap_sync_all
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
@ -35,14 +33,6 @@ class LDAPSyncTests(TestCase):
additional_group_dn="ou=groups",
)
def test_sync_missing_page(self):
"""Test sync with missing page"""
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync.delay(self.source.pk, class_to_path(UserLDAPSynchronizer), "foo").get()
status = TaskInfo.by_name("ldap_sync:ldap:users:foo")
self.assertEqual(status.result.status, TaskResultStatus.ERROR)
def test_sync_error(self):
"""Test user sync"""
self.source.property_mappings.set(

View File

@ -3213,6 +3213,7 @@
"authentik.crypto",
"authentik.events",
"authentik.flows",
"authentik.lib",
"authentik.outposts",
"authentik.policies.dummy",
"authentik.policies.event_matcher",
@ -3983,7 +3984,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"
@ -4184,7 +4185,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"
@ -4389,7 +4390,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"
@ -6555,7 +6556,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"
@ -7300,7 +7301,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"
@ -8386,7 +8387,7 @@
"type": {
"type": "string",
"enum": [
"internal",
"default",
"external",
"service_account",
"internal_service_account"

View File

@ -0,0 +1,36 @@
# This file is used for development and debugging, and should not be used for production instances
version: '3.5'
services:
flower:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.4}
restart: unless-stopped
command: worker-status
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
env_file:
- .env
ports:
- "9001:9000"
depends_on:
- postgresql
- redis
server:
environment:
AUTHENTIK_REMOTE_DEBUG: "true"
PYDEVD_THREAD_DUMP_ON_WARN_EVALUATION_TIMEOUT: "true"
ports:
- 6800:6800
worker:
environment:
CELERY_RDB_HOST: "0.0.0.0"
CELERY_RDBSIG: "1"
AUTHENTIK_REMOTE_DEBUG: "true"
PYDEVD_THREAD_DUMP_ON_WARN_EVALUATION_TIMEOUT: "true"
ports:
- 6900:6900

2
go.mod
View File

@ -26,7 +26,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
goauthentik.io/api/v3 v3.2023061.6
goauthentik.io/api/v3 v3.2023061.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0

4
go.sum
View File

@ -1070,8 +1070,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023061.6 h1:4zbo0Dtx42HLYObizIlTWAk7iBvCv9kmCvzBxMElkIk=
goauthentik.io/api/v3 v3.2023061.6/go.mod h1:tC7qK9VSP0zJah5p5xHFnjZt/4dAkXVwcrWyZNGYhwQ=
goauthentik.io/api/v3 v3.2023061.3 h1:uD+M52JTba2w8n5rWQKoPQ3gD0BDQuLNaDx/G0rKwKE=
goauthentik.io/api/v3 v3.2023061.3/go.mod h1:tC7qK9VSP0zJah5p5xHFnjZt/4dAkXVwcrWyZNGYhwQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

@ -8,7 +8,6 @@ import (
"net/http/cookiejar"
"net/url"
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/prometheus/client_golang/prometheus"
@ -22,20 +21,10 @@ import (
var (
FlowTimingGet = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_flow_timing_get_seconds",
Help: "Duration it took to get a challenge in seconds",
}, []string{"stage", "flow"})
FlowTimingPost = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_flow_timing_post_seconds",
Help: "Duration it took to send a challenge in seconds",
}, []string{"stage", "flow"})
// NOTE: the following metrics are kept for compatibility purpose
FlowTimingGetLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_flow_timing_get",
Help: "Duration it took to get a challenge",
}, []string{"stage", "flow"})
FlowTimingPostLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
FlowTimingPost = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_flow_timing_post",
Help: "Duration it took to send a challenge",
}, []string{"stage", "flow"})
@ -197,10 +186,6 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
FlowTimingGet.With(prometheus.Labels{
"stage": ch.GetComponent(),
"flow": fe.flowSlug,
}).Observe(float64(gcsp.EndTime.Sub(gcsp.StartTime)) / float64(time.Second))
FlowTimingGetLegacy.With(prometheus.Labels{
"stage": ch.GetComponent(),
"flow": fe.flowSlug,
}).Observe(float64(gcsp.EndTime.Sub(gcsp.StartTime)))
return challenge, nil
}
@ -258,10 +243,6 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
FlowTimingPost.With(prometheus.Labels{
"stage": ch.GetComponent(),
"flow": fe.flowSlug,
}).Observe(float64(scsp.EndTime.Sub(scsp.StartTime)) / float64(time.Second))
FlowTimingPostLegacy.With(prometheus.Labels{
"stage": ch.GetComponent(),
"flow": fe.flowSlug,
}).Observe(float64(scsp.EndTime.Sub(scsp.StartTime)))
if depth >= 10 {

View File

@ -2,7 +2,6 @@ package ldap
import (
"net"
"time"
"beryju.io/ldap"
"github.com/getsentry/sentry-go"
@ -21,11 +20,6 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
"outpost_name": ls.ac.Outpost.Name,
"type": "bind",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "bind",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
}()
@ -55,12 +49,6 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
"reason": "no_provider",
"app": "",
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "bind",
"reason": "no_provider",
"app": "",
}).Inc()
return ldap.LDAPResultInsufficientAccessRights, nil
}

View File

@ -52,12 +52,6 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
"reason": "flow_error",
"app": db.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "flow_error",
"app": db.si.GetAppSlug(),
}).Inc()
req.Log().WithError(err).Warning("failed to execute flow")
return ldap.LDAPResultInvalidCredentials, nil
}
@ -68,12 +62,6 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
"reason": "invalid_credentials",
"app": db.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "invalid_credentials",
"app": db.si.GetAppSlug(),
}).Inc()
req.Log().Info("Invalid credentials")
return ldap.LDAPResultInvalidCredentials, nil
}
@ -87,12 +75,6 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
"reason": "access_denied",
"app": db.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "access_denied",
"app": db.si.GetAppSlug(),
}).Inc()
return ldap.LDAPResultInsufficientAccessRights, nil
}
if err != nil {
@ -102,12 +84,6 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
"reason": "access_check_fail",
"app": db.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "access_check_fail",
"app": db.si.GetAppSlug(),
}).Inc()
req.Log().WithError(err).Warning("failed to check access")
return ldap.LDAPResultOperationsError, nil
}
@ -122,12 +98,6 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
"reason": "user_info_fail",
"app": db.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "user_info_fail",
"app": db.si.GetAppSlug(),
}).Inc()
req.Log().WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil
}

View File

@ -15,20 +15,10 @@ import (
var (
Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_ldap_request_duration_seconds",
Help: "LDAP request latencies in seconds",
}, []string{"outpost_name", "type", "app"})
RequestsRejected = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authentik_outpost_ldap_requests_rejected_total",
Help: "Total number of rejected requests",
}, []string{"outpost_name", "type", "reason", "app"})
// NOTE: the following metrics are kept for compatibility purpose
RequestsLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_ldap_requests",
Help: "The total number of configured providers",
}, []string{"outpost_name", "type", "app"})
RequestsRejectedLegacy = promauto.NewCounterVec(prometheus.CounterOpts{
RequestsRejected = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authentik_outpost_ldap_requests_rejected",
Help: "Total number of rejected requests",
}, []string{"outpost_name", "type", "reason", "app"})

View File

@ -2,7 +2,6 @@ package ldap
import (
"net"
"time"
"beryju.io/ldap"
"github.com/getsentry/sentry-go"
@ -22,11 +21,6 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
"outpost_name": ls.ac.Outpost.Name,
"type": "search",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "search",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
req.Log().WithField("attributes", searchReq.Attributes).WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
}()

View File

@ -45,12 +45,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "empty_bind_dn",
"app": ds.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "empty_bind_dn",
"app": ds.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
}
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
@ -60,12 +54,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "invalid_bind_dn",
"app": ds.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "invalid_bind_dn",
"app": ds.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ds.si.GetBaseDN())
}
@ -78,12 +66,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "user_info_not_cached",
"app": ds.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "user_info_not_cached",
"app": ds.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
}
accsp.Finish()
@ -96,12 +78,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "filter_parse_fail",
"app": ds.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "filter_parse_fail",
"app": ds.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}

View File

@ -62,12 +62,6 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "empty_bind_dn",
"app": ms.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
"type": "search",
"reason": "empty_bind_dn",
"app": ms.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
}
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
@ -77,12 +71,6 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "invalid_bind_dn",
"app": ms.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
"type": "search",
"reason": "invalid_bind_dn",
"app": ms.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ms.si.GetBaseDN())
}
@ -95,12 +83,6 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
"reason": "user_info_not_cached",
"app": ms.si.GetAppSlug(),
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
"type": "search",
"reason": "user_info_not_cached",
"app": ms.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
}
accsp.Finish()

View File

@ -2,10 +2,9 @@ package ldap
import (
"net"
"time"
"beryju.io/ldap"
"github.com/getsentry/sentry-go"
"beryju.io/ldap"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/ldap/bind"
@ -21,11 +20,6 @@ func (ls *LDAPServer) Unbind(boundDN string, conn net.Conn) (ldap.LDAPResultCode
"outpost_name": ls.ac.Outpost.Name,
"type": "unbind",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "unbind",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Unbind request")
}()
@ -55,11 +49,5 @@ func (ls *LDAPServer) Unbind(boundDN string, conn net.Conn) (ldap.LDAPResultCode
"reason": "no_provider",
"app": "",
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "unbind",
"reason": "no_provider",
"app": "",
}).Inc()
return ldap.LDAPResultOperationsError, nil
}

View File

@ -163,19 +163,13 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
}
before := time.Now()
inner.ServeHTTP(rw, r)
elapsed := time.Since(before)
after := time.Since(before)
metrics.Requests.With(prometheus.Labels{
"outpost_name": a.outpostName,
"type": "app",
"method": r.Method,
"host": web.GetHost(r),
}).Observe(float64(elapsed) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": a.outpostName,
"type": "app",
"method": r.Method,
"host": web.GetHost(r),
}).Observe(float64(elapsed))
}).Observe(float64(after))
})
})
if server.API().GlobalConfig.ErrorReporting.Enabled {

View File

@ -55,7 +55,7 @@ func (a *Application) configureProxy() error {
}
before := time.Now()
rp.ServeHTTP(rw, r)
elapsed := time.Since(before)
after := time.Since(before)
metrics.UpstreamTiming.With(prometheus.Labels{
"outpost_name": a.outpostName,
@ -63,14 +63,7 @@ func (a *Application) configureProxy() error {
"method": r.Method,
"scheme": r.URL.Scheme,
"host": web.GetHost(r),
}).Observe(float64(elapsed) / float64(time.Second))
metrics.UpstreamTimingLegacy.With(prometheus.Labels{
"outpost_name": a.outpostName,
"upstream_host": r.URL.Host,
"method": r.Method,
"scheme": r.URL.Scheme,
"host": web.GetHost(r),
}).Observe(float64(elapsed))
}).Observe(float64(after))
})
return nil
}

View File

@ -19,37 +19,25 @@ import (
func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) {
before := time.Now()
rw.WriteHeader(204)
elapsed := time.Since(before)
after := time.Since(before)
metrics.Requests.With(prometheus.Labels{
"outpost_name": ps.akAPI.Outpost.Name,
"method": r.Method,
"host": web.GetHost(r),
"type": "ping",
}).Observe(float64(elapsed) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": ps.akAPI.Outpost.Name,
"method": r.Method,
"host": web.GetHost(r),
"type": "ping",
}).Observe(float64(elapsed))
}).Observe(float64(after))
}
func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) {
before := time.Now()
web.DisableIndex(http.StripPrefix("/outpost.goauthentik.io/static/dist", staticWeb.StaticHandler)).ServeHTTP(rw, r)
elapsed := time.Since(before)
after := time.Since(before)
metrics.Requests.With(prometheus.Labels{
"outpost_name": ps.akAPI.Outpost.Name,
"method": r.Method,
"host": web.GetHost(r),
"type": "static",
}).Observe(float64(elapsed) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": ps.akAPI.Outpost.Name,
"method": r.Method,
"host": web.GetHost(r),
"type": "static",
}).Observe(float64(elapsed))
}).Observe(float64(after))
}
func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, string) {

View File

@ -15,20 +15,10 @@ import (
var (
Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_proxy_request_duration_seconds",
Help: "Proxy request latencies in seconds",
}, []string{"outpost_name", "method", "host", "type"})
UpstreamTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_proxy_upstream_response_duration_seconds",
Help: "Proxy upstream response latencies in seconds",
}, []string{"outpost_name", "method", "scheme", "host", "upstream_host"})
// NOTE: the following metric is kept for compatibility purpose
RequestsLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_proxy_requests",
Help: "The total number of configured providers",
}, []string{"outpost_name", "method", "host", "type"})
UpstreamTimingLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
UpstreamTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_proxy_upstream_time",
Help: "A summary of the duration we wait for the upstream reply",
}, []string{"outpost_name", "method", "scheme", "host", "upstream_host"})

View File

@ -32,11 +32,6 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
"reason": "flow_error",
"app": r.pi.appSlug,
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "flow_error",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
}
@ -46,11 +41,6 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
"reason": "invalid_credentials",
"app": r.pi.appSlug,
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "invalid_credentials",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
}
@ -63,11 +53,6 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
"reason": "access_check_fail",
"app": r.pi.appSlug,
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_check_fail",
"app": r.pi.appSlug,
}).Inc()
return
}
if !access {
@ -78,11 +63,6 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
"reason": "access_denied",
"app": r.pi.appSlug,
}).Inc()
metrics.RequestsRejectedLegacy.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_denied",
"app": r.pi.appSlug,
}).Inc()
return
}
_ = w.Write(r.Response(radius.CodeAccessAccept))

View File

@ -2,7 +2,6 @@ package radius
import (
"crypto/sha512"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
@ -46,10 +45,6 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
metrics.Requests.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)) / float64(time.Second))
metrics.RequestsLegacy.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
}()

View File

@ -15,20 +15,10 @@ import (
var (
Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_radius_request_duration_seconds",
Help: "RADIUS request latencies in seconds",
}, []string{"outpost_name", "app"})
RequestsRejected = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authentik_outpost_radius_requests_rejected_total",
Help: "Total number of rejected requests",
}, []string{"outpost_name", "reason", "app"})
// NOTE: the following metric is kept for compatibility purpose
RequestsLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_outpost_radius_requests",
Help: "The total number of successful requests",
}, []string{"outpost_name", "app"})
RequestsRejectedLegacy = promauto.NewCounterVec(prometheus.CounterOpts{
RequestsRejected = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authentik_outpost_radius_requests_rejected",
Help: "Total number of rejected requests",
}, []string{"outpost_name", "reason", "app"})

View File

@ -15,12 +15,6 @@ import (
var (
Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_main_request_duration_seconds",
Help: "API request latencies in seconds",
}, []string{"dest"})
// NOTE: the following metric is kept for compatibility purpose
RequestsLegacy = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authentik_main_requests",
Help: "The total number of configured providers",
}, []string{"dest"})

View File

@ -34,13 +34,9 @@ func (ws *WebServer) configureProxy() {
if ws.ProxyServer != nil {
before := time.Now()
ws.ProxyServer.Handle(rw, r)
elapsed := time.Since(before)
Requests.With(prometheus.Labels{
"dest": "embedded_outpost",
}).Observe(float64(elapsed) / float64(time.Second))
RequestsLegacy.With(prometheus.Labels{
"dest": "embedded_outpost",
}).Observe(float64(elapsed))
}).Observe(float64(time.Since(before)))
return
}
ws.proxyErrorHandler(rw, r, fmt.Errorf("proxy not running"))
@ -56,23 +52,15 @@ func (ws *WebServer) configureProxy() {
before := time.Now()
if ws.ProxyServer != nil {
if ws.ProxyServer.HandleHost(rw, r) {
elapsed := time.Since(before)
Requests.With(prometheus.Labels{
"dest": "embedded_outpost",
}).Observe(float64(elapsed) / float64(time.Second))
RequestsLegacy.With(prometheus.Labels{
"dest": "embedded_outpost",
}).Observe(float64(elapsed))
}).Observe(float64(time.Since(before)))
return
}
}
elapsed := time.Since(before)
Requests.With(prometheus.Labels{
"dest": "core",
}).Observe(float64(elapsed) / float64(time.Second))
RequestsLegacy.With(prometheus.Labels{
"dest": "core",
}).Observe(float64(elapsed))
}).Observe(float64(time.Since(before)))
r.Body = http.MaxBytesReader(rw, r.Body, 32*1024*1024)
rp.ServeHTTP(rw, r)
}))

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-28 11:50+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"
@ -427,7 +427,7 @@ msgstr ""
msgid "Webhook Mappings"
msgstr ""
#: authentik/events/monitored_tasks.py:205
#: authentik/events/monitored_tasks.py:198
msgid "Task has not been run yet."
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."
@ -2017,33 +2017,33 @@ msgstr ""
msgid "Dummy Stages"
msgstr ""
#: authentik/stages/email/models.py:26
#: authentik/stages/email/models.py:25
msgid "Password Reset"
msgstr ""
#: authentik/stages/email/models.py:30
#: authentik/stages/email/models.py:29
msgid "Account Confirmation"
msgstr ""
#: authentik/stages/email/models.py:59
#: authentik/stages/email/models.py:58
msgid ""
"When enabled, global Email connection settings will be used and connection "
"settings below will be ignored."
msgstr ""
#: authentik/stages/email/models.py:74
#: authentik/stages/email/models.py:73
msgid "Activate users upon completion of stage."
msgstr ""
#: authentik/stages/email/models.py:78
#: authentik/stages/email/models.py:77
msgid "Time in minutes the token sent is valid."
msgstr ""
#: authentik/stages/email/models.py:132
#: authentik/stages/email/models.py:122
msgid "Email Stage"
msgstr ""
#: authentik/stages/email/models.py:133
#: authentik/stages/email/models.py:123
msgid "Email Stages"
msgstr ""

File diff suppressed because it is too large Load Diff

83
poetry.lock generated
View File

@ -253,13 +253,13 @@ files = [
[[package]]
name = "astroid"
version = "2.15.6"
version = "2.15.5"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"},
{file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"},
{file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"},
{file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"},
]
[package.dependencies]
@ -1340,13 +1340,13 @@ tests = ["black", "django-stubs[compatible-mypy]", "djangorestframework-stubs[co
[[package]]
name = "drf-spectacular"
version = "0.26.4"
version = "0.26.3"
description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework"
optional = false
python-versions = ">=3.6"
files = [
{file = "drf-spectacular-0.26.4.tar.gz", hash = "sha256:8f5a8f87353d1bb8dcb3f3909b7109b2dcbe1d91f3e069409cf322963e140bd6"},
{file = "drf_spectacular-0.26.4-py3-none-any.whl", hash = "sha256:afeccc6533dcdb4e78afbfcc49f3c5e9c369aeb62f965e4d1a43b165449c147a"},
{file = "drf-spectacular-0.26.3.tar.gz", hash = "sha256:b907a72a0244e5dcfeca625e9632cd8ebccdbe2cb528b7c1de1191708be6f31e"},
{file = "drf_spectacular-0.26.3-py3-none-any.whl", hash = "sha256:1d84ac70522baaadd6d84a25ce5fe5ea50cfcba0387856689f98ac536f14aa32"},
]
[package.dependencies]
@ -1809,31 +1809,6 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jsonpatch"
version = "1.33"
description = "Apply JSON-Patches (RFC 6902)"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
files = [
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
]
[package.dependencies]
jsonpointer = ">=1.9"
[[package]]
name = "jsonpointer"
version = "2.4"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
files = [
{file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
{file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
]
[[package]]
name = "jsonschema"
version = "4.17.3"
@ -2724,17 +2699,17 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pylint"
version = "2.17.5"
version = "2.17.4"
description = "python code static checker"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"},
{file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"},
{file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"},
{file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"},
]
[package.dependencies]
astroid = ">=2.15.6,<=2.17.0-dev0"
astroid = ">=2.15.4,<=2.17.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""}
isort = ">=4.2.5,<6"
@ -3173,28 +3148,28 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.0.280"
version = "0.0.278"
description = "An extremely fast Python linter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.280-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec"},
{file = "ruff-0.0.280-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4"},
{file = "ruff-0.0.280-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c"},
{file = "ruff-0.0.280-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe"},
{file = "ruff-0.0.280-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6"},
{file = "ruff-0.0.280-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189"},
{file = "ruff-0.0.280-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc"},
{file = "ruff-0.0.280-py3-none-win32.whl", hash = "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763"},
{file = "ruff-0.0.280-py3-none-win_amd64.whl", hash = "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30"},
{file = "ruff-0.0.280-py3-none-win_arm64.whl", hash = "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d"},
{file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"},
{file = "ruff-0.0.278-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:1a90ebd8f2a554db1ee8d12b2f3aa575acbd310a02cd1a9295b3511a4874cf98"},
{file = "ruff-0.0.278-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:38ca1c0c8c1221fe64c0a66784c91501d09a8ed02a4dbfdc117c0ce32a81eefc"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c62a0bde4d20d087cabce2fa8b012d74c2e985da86d00fb3359880469b90e31"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7545bb037823cd63dca19280f75a523a68bd3e78e003de74609320d6822b5a52"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb380d2d6fdb60656a0b5fa78305535db513fc72ce11f4532cc1641204ef380"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d11149c7b186f224f2055e437a030cd83b164a43cc0211314c33ad1553ed9c4c"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666e739fb2685277b879d493848afe6933e3be30d40f41fe0e571ad479d57d77"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec8b0469b54315803aaf1fbf9a37162a3849424cab6182496f972ad56e0ea702"},
{file = "ruff-0.0.278-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c25b96602695a147d62a572865b753ef56aff1524abab13b9436724df30f9bd7"},
{file = "ruff-0.0.278-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a48621f5f372d5019662db5b3dbfc5f1450f927683d75f1153fe0ebf20eb9698"},
{file = "ruff-0.0.278-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1078125123a3c68e92463afacedb7e41b15ccafc09e510c6c755a23087afc8de"},
{file = "ruff-0.0.278-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3ce0d620e257b4cad16e2f0c103b2f43a07981668a3763380542e8a131d11537"},
{file = "ruff-0.0.278-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1cae4c07d334eb588f171f1363fa89a8911047eb93184276be11a24dbbc996c7"},
{file = "ruff-0.0.278-py3-none-win32.whl", hash = "sha256:70d39f5599d8449082ab8ce542fa98e16413145eb411dd1dc16575b44565d52d"},
{file = "ruff-0.0.278-py3-none-win_amd64.whl", hash = "sha256:e131595ab7f4ce61a1650463bd2fe304b49e7d0deb0dfa664b92817c97cdba5f"},
{file = "ruff-0.0.278-py3-none-win_arm64.whl", hash = "sha256:737a0cfb6c36aaa92d97a46957dfd5e55329299074ad06ed12663b98e0c6fc82"},
{file = "ruff-0.0.278.tar.gz", hash = "sha256:1a9f1d925204cfba81b18368b7ac943befcfccc3a41e170c91353b674c6b7a66"},
]
[[package]]
@ -4211,4 +4186,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "ab00edcd235c1c92dad9a91ace11d50df4564297193683cca7aa2b207ca27be6"
content-hash = "06466753c4ce0063905809123b1e2bb444034d84acdd108dcb20a9f92ce12fa6"

View File

@ -172,7 +172,6 @@ webauthn = "*"
wsproto = "*"
xmlsec = "*"
zxcvbn = "*"
jsonpatch = "*"
[tool.poetry.dev-dependencies]
bandit = "*"

View File

@ -3633,60 +3633,78 @@ paths:
operationId: core_tenants_list
description: Tenant Viewset
parameters:
- in: query
name: branding_favicon
- name: branding_favicon
required: false
in: query
description: branding_favicon
schema:
type: string
- in: query
name: branding_logo
- name: branding_logo
required: false
in: query
description: branding_logo
schema:
type: string
- in: query
name: branding_title
- name: branding_title
required: false
in: query
description: branding_title
schema:
type: string
- in: query
name: default
schema:
type: boolean
- in: query
name: domain
- name: default
required: false
in: query
description: default
schema:
type: string
- in: query
name: event_retention
- name: domain
required: false
in: query
description: domain
schema:
type: string
- in: query
name: flow_authentication
- name: event_retention
required: false
in: query
description: event_retention
schema:
type: string
format: uuid
- in: query
name: flow_device_code
- name: flow_authentication
required: false
in: query
description: flow_authentication
schema:
type: string
format: uuid
- in: query
name: flow_invalidation
- name: flow_device_code
required: false
in: query
description: flow_device_code
schema:
type: string
format: uuid
- in: query
name: flow_recovery
- name: flow_invalidation
required: false
in: query
description: flow_invalidation
schema:
type: string
format: uuid
- in: query
name: flow_unenrollment
- name: flow_recovery
required: false
in: query
description: flow_recovery
schema:
type: string
format: uuid
- in: query
name: flow_user_settings
- name: flow_unenrollment
required: false
in: query
description: flow_unenrollment
schema:
type: string
- name: flow_user_settings
required: false
in: query
description: flow_user_settings
schema:
type: string
format: uuid
- name: ordering
required: false
in: query
@ -3711,16 +3729,18 @@ paths:
description: A search term.
schema:
type: string
- in: query
name: tenant_uuid
- name: tenant_uuid
required: false
in: query
description: tenant_uuid
schema:
type: string
format: uuid
- in: query
name: web_certificate
- name: web_certificate
required: false
in: query
description: web_certificate
schema:
type: string
format: uuid
tags:
- core
security:
@ -4589,12 +4609,12 @@ paths:
schema:
type: string
enum:
- default
- external
- internal
- internal_service_account
- service_account
description: |-
* `internal` - Internal
* `default` - Default
* `external` - External
* `service_account` - Service Account
* `internal_service_account` - Internal Service Account
@ -5143,12 +5163,16 @@ paths:
schema:
type: boolean
default: true
- in: query
name: managed
- name: managed
required: false
in: query
description: managed
schema:
type: string
- in: query
name: name
- name: name
required: false
in: query
description: name
schema:
type: string
- name: ordering
@ -26665,6 +26689,7 @@ components:
- authentik.crypto
- authentik.events
- authentik.flows
- authentik.lib
- authentik.outposts
- authentik.policies.dummy
- authentik.policies.event_matcher
@ -26714,6 +26739,7 @@ components:
* `authentik.crypto` - authentik Crypto
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
* `authentik.outposts` - authentik Outpost
* `authentik.policies.dummy` - authentik Policies.Dummy
* `authentik.policies.event_matcher` - authentik Policies.Event Matcher
@ -29440,6 +29466,7 @@ components:
* `authentik.crypto` - authentik Crypto
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
* `authentik.outposts` - authentik Outpost
* `authentik.policies.dummy` - authentik Policies.Dummy
* `authentik.policies.event_matcher` - authentik Policies.Event Matcher
@ -29630,6 +29657,7 @@ components:
* `authentik.crypto` - authentik Crypto
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
* `authentik.outposts` - authentik Outpost
* `authentik.policies.dummy` - authentik Policies.Dummy
* `authentik.policies.event_matcher` - authentik Policies.Event Matcher
@ -36726,6 +36754,7 @@ components:
* `authentik.crypto` - authentik Crypto
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
* `authentik.outposts` - authentik Outpost
* `authentik.policies.dummy` - authentik Policies.Dummy
* `authentik.policies.event_matcher` - authentik Policies.Event Matcher
@ -42552,13 +42581,13 @@ components:
- user
UserTypeEnum:
enum:
- internal
- default
- external
- service_account
- internal_service_account
type: string
description: |-
* `internal` - Internal
* `default` - Default
* `external` - External
* `service_account` - Service Account
* `internal_service_account` - Internal Service Account

View File

@ -35,19 +35,6 @@ class OutpostKubernetesTests(TestCase):
service_connection=self.service_connection,
)
self.outpost.providers.add(self.provider)
self.outpost.config.kubernetes_json_patches = {
"deployment": [
{
"op": "add",
"path": "/spec/template/spec/containers/0/resources",
"value": {
"requests": {"cpu": "2000m", "memory": "2000Mi"},
"limits": {"cpu": "4000m", "memory": "8000Mi"},
},
}
]
}
self.outpost.providers.add(self.provider)
self.outpost.save()
def test_deployment_reconciler(self):
@ -59,18 +46,6 @@ class OutpostKubernetesTests(TestCase):
config = self.outpost.config
config.kubernetes_replicas = 3
config.kubernetes_json_patches = {
"deployment": [
{
"op": "add",
"path": "/spec/template/spec/containers/0/resources",
"value": {
"requests": {"cpu": "1000m", "memory": "2000Mi"},
"limits": {"cpu": "2000m", "memory": "4000Mi"},
},
}
]
}
self.outpost.config = config
with self.assertRaises(NeedsUpdate):

View File

@ -15,7 +15,6 @@
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,

4222
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,28 +30,28 @@
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/legacy-modes": "^6.3.2",
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.4.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.6.1-1690455444",
"@goauthentik/api": "^2023.6.1-1689715510",
"@lit-labs/context": "^0.3.3",
"@lit-labs/task": "^2.1.2",
"@lit/localize": "^0.11.4",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.60.1",
"@sentry/tracing": "^7.60.1",
"@sentry/browser": "^7.59.3",
"@sentry/tracing": "^7.59.3",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.3.2",
"chart.js": "^4.3.0",
"chartjs-adapter-moment": "^1.0.1",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.32.0",
"core-js": "^3.31.1",
"country-flag-icons": "^1.5.7",
"fuse.js": "^6.6.2",
"lit": "^2.7.6",
"mermaid": "^10.3.0",
"mermaid": "^10.2.4",
"rapidoc": "^9.3.4",
"style-mod": "^4.0.3",
"webcomponent-qr-code": "^1.2.0",
@ -75,17 +75,17 @@
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.2",
"@storybook/addon-essentials": "^7.1.1",
"@storybook/addon-links": "^7.1.1",
"@storybook/blocks": "^7.1.1",
"@storybook/addon-essentials": "^7.1.0",
"@storybook/addon-links": "^7.1.0",
"@storybook/blocks": "^7.1.0",
"@storybook/web-components": "^7.1.0",
"@storybook/web-components-vite": "^7.1.1",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@storybook/web-components-vite": "^7.1.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.8",
"@types/grecaptcha": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tsconfig-paths": "^1.0.3",
"eslint": "^8.45.0",
@ -95,8 +95,8 @@
"eslint-plugin-storybook": "^0.6.13",
"lit-analyzer": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.0",
"pyright": "^1.1.319",
"prettier": "^2.8.8",
"pyright": "^1.1.318",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^2.79.1",
@ -106,17 +106,17 @@
"rollup-plugin-postcss-lit": "^2.1.0",
"rollup-plugin-terser": "^7.0.2",
"sharp-cli": "^4.1.1",
"storybook": "^7.1.1",
"storybook": "^7.1.0",
"storybook-addon-mock": "^4.1.0",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.6.1",
"tslib": "^2.6.0",
"turnstile-types": "^1.1.2",
"typescript": "^5.1.6",
"vite-tsconfig-paths": "^4.2.0"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.18.17",
"@esbuild/darwin-arm64": "^0.18.13",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.18.17"
"@esbuild/linux-arm64": "^0.18.14"
}
}

View File

@ -92,10 +92,8 @@ export class RecentEventsCard extends Table<Event> {
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
return super.renderEmpty(html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`);
}
}

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/applications/wizard/ApplicationWizard";
import { PFSize } from "@goauthentik/app/elements/Spinner";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import MDApplication from "@goauthentik/docs/core/applications.md";
@ -12,12 +11,13 @@ import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@goauthentik/user/LibraryApplication/AppIcon";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { Application, CoreApi } from "@goauthentik/api";
@ -56,6 +56,7 @@ export class ApplicationListPage extends TablePage<Application> {
static get styles(): CSSResult[] {
return super.styles.concat(
PFAvatar,
PFCard,
css`
/* Fix alignment issues with images in tables */
@ -124,9 +125,24 @@ export class ApplicationListPage extends TablePage<Application> {
</ak-forms-delete-bulk>`;
}
renderIcon(item: Application): TemplateResult {
if (item?.metaIcon) {
if (item.metaIcon.startsWith("fa://")) {
const icon = item.metaIcon.replaceAll("fa://", "");
return html`<i class="fas ${icon}"></i>`;
}
return html`<img
class="app-icon pf-c-avatar"
src="${ifDefined(item.metaIcon)}"
alt="${msg("Application Icon")}"
/>`;
}
return html`<i class="fas fa-share-square"></i>`;
}
row(item: Application): TemplateResult[] {
return [
html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`,
this.renderIcon(item),
html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}

View File

@ -2,7 +2,6 @@ import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
import { PFSize } from "@goauthentik/app/elements/Spinner";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
@ -10,7 +9,6 @@ import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/ObjectChangelog";
import "@goauthentik/user/LibraryApplication/AppIcon";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
@ -82,15 +80,11 @@ export class ApplicationViewPage extends AKElement {
render(): TemplateResult {
return html`<ak-page-header
icon=${this.application?.metaIcon || ""}
header=${this.application?.name || msg("Loading")}
description=${ifDefined(this.application?.metaPublisher)}
.iconImage=${true}
>
<ak-app-icon
size=${PFSize.Small}
slot="icon"
.app=${this.application}
></ak-app-icon>
</ak-page-header>
${this.renderApp()}`;
}

View File

@ -92,24 +92,6 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
];
}
// TODO: Make this more generic, maybe automatically get the plural name
// of the object to use in the renderEmpty
renderEmpty(inner?: TemplateResult): TemplateResult {
return super.renderEmpty(html`
${inner
? inner
: html`<ak-empty-state
icon=${this.pageIcon()}
header="${msg("No licenses found.")}"
>
<div slot="body">
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
</div>
<div slot="primary">${this.renderObjectCreate()}</div>
</ak-empty-state>`}
`);
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
@ -167,9 +149,9 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
<ak-aggregate-card
class="pf-l-grid__item"
icon="pf-icon pf-icon-user"
header=${msg("Forecast internal users")}
header=${msg("Forecast default users")}
subtext=${msg(
str`Estimated user count one year from now based on ${this.forecast?.users} current internal users and ${this.forecast?.forecastedUsers} forecasted internal users.`,
str`Estimated user count one year from now based on ${this.forecast?.users} current users and ${this.forecast?.forecastedUsers} forecasted users.`,
)}
>
~&nbsp;${(this.forecast?.users || 0) +

View File

@ -121,22 +121,23 @@ export class BoundStagesList extends Table<FlowStageBinding> {
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Stages bound")} icon="pf-icon-module">
<div slot="body">${msg("No stages are currently bound to this flow.")}</div>
<div slot="primary">
<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Stage binding")} </span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind stage")}
</button>
</ak-forms-modal>
</div>
</ak-empty-state>`,
);
return super.renderEmpty(html`<ak-empty-state
header=${msg("No Stages bound")}
icon="pf-icon-module"
>
<div slot="body">${msg("No stages are currently bound to this flow.")}</div>
<div slot="primary">
<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Stage binding")} </span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind stage")}
</button>
</ak-forms-modal>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {

View File

@ -40,14 +40,16 @@ export class FlowViewPage extends AKElement {
flow!: Flow;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFDescriptionList, PFButton, PFCard, PFContent, PFGrid].concat(css`
img.pf-icon {
max-height: 24px;
}
ak-tabs {
height: 100%;
}
`);
return [PFBase, PFPage, PFDescriptionList, PFButton, PFCard, PFContent, PFGrid].concat(
css`
img.pf-icon {
max-height: 24px;
}
ak-tabs {
height: 100%;
}
`,
);
}
render(): TemplateResult {

View File

@ -170,26 +170,27 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Policies bound.")} icon="pf-icon-module">
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<div slot="primary">
<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Binding")} </span>
<ak-policy-binding-form
slot="form"
targetPk=${ifDefined(this.target)}
?policyOnly=${this.policyOnly}
>
</ak-policy-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Create Binding")}
</button>
</ak-forms-modal>
</div>
</ak-empty-state>`,
);
return super.renderEmpty(html`<ak-empty-state
header=${msg("No Policies bound.")}
icon="pf-icon-module"
>
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<div slot="primary">
<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Binding")} </span>
<ak-policy-binding-form
slot="form"
targetPk=${ifDefined(this.target)}
?policyOnly=${this.policyOnly}
>
</ak-policy-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Create Binding")}
</button>
</ak-forms-modal>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {

View File

@ -191,8 +191,9 @@ export class LDAPProviderViewPage extends AKElement {
class="pf-c-form-control"
readonly
type="text"
value=${`cn=${this.me?.user
.username},ou=users,${this.provider?.baseDn?.toLowerCase()}`}
value=${`cn=${
this.me?.user.username
},ou=users,${this.provider?.baseDn?.toLowerCase()}`}
/>
</div>
<div class="pf-c-form__group">

View File

@ -120,62 +120,58 @@ export class SAMLProviderViewPage extends AKElement {
renderRelatedObjects(): TemplateResult {
const relatedObjects = [];
if (this.provider?.assignedApplicationName) {
relatedObjects.push(
html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Metadata")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${ifDefined(this.provider?.urlDownloadMetadata)}
>
${msg("Download")}
</a>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
if (!navigator.clipboard) {
return Promise.resolve(
showMessage({
level: MessageLevel.info,
message: this.provider?.urlDownloadMetadata || "",
}),
);
}
return navigator.clipboard.writeText(
this.provider?.urlDownloadMetadata || "",
relatedObjects.push(html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Metadata")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${ifDefined(this.provider?.urlDownloadMetadata)}
>
${msg("Download")}
</a>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
if (!navigator.clipboard) {
return Promise.resolve(
showMessage({
level: MessageLevel.info,
message: this.provider?.urlDownloadMetadata || "",
}),
);
}}
>
${msg("Copy download URL")}
</ak-action-button>
</div>
</dd>
</div>`,
);
}
return navigator.clipboard.writeText(
this.provider?.urlDownloadMetadata || "",
);
}}
>
${msg("Copy download URL")}
</ak-action-button>
</div>
</dd>
</div>`);
}
if (this.signer) {
relatedObjects.push(
html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Download signing certificate")}</span
relatedObjects.push(html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Download signing certificate")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
href=${this.signer.certificateDownloadUrl}
>${msg("Download")}</a
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
href=${this.signer.certificateDownloadUrl}
>${msg("Download")}</a
>
</div>
</dd>
</div>`,
);
</div>
</dd>
</div>`);
}
return html` <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("Related objects")}</div>

View File

@ -80,27 +80,22 @@ export class UserForm extends ModelForm<User, number> {
<ak-form-element-horizontal label=${msg("User type")} ?required=${true} name="type">
<ak-radio
.options=${[
// TODO: Add better copy
{
label: "Internal",
value: UserTypeEnum.Internal,
label: "Default",
value: UserTypeEnum.Default,
default: true,
description: html`${msg(
"Internal users might be users such as company employees, which will get access to the full Enterprise feature set.",
)}`,
description: html`${msg("Default user")}`,
},
{
label: "External",
value: UserTypeEnum.External,
description: html`${msg(
"External users might be external consultants or B2C customers. These users don't get access to enterprise features.",
)}`,
description: html`${msg("External user")}`,
},
{
label: "Service account",
value: UserTypeEnum.ServiceAccount,
description: html`${msg(
"Service accounts should be used for machine-to-machine authentication or other automations.",
)}`,
description: html`${msg("Service account")}`,
},
]}
.value=${this.instance?.type}

View File

@ -9,9 +9,6 @@
--ak-dark-background-light: #1c1e21;
--ak-dark-background-light-ish: #212427;
--ak-dark-background-lighter: #2b2e33;
/* PatternFly likes to override global variables for some reason */
--ak-global--Color--100: var(--pf-global--Color--100);
}
::-webkit-scrollbar {

View File

@ -1,12 +1,11 @@
:root {
--pf-global--Color--100: var(--ak-dark-foreground) !important;
--ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--link--Color: var(--ak-dark-foreground-link) !important;
}
body {
background-color: var(--ak-dark-background) !important;
}
:root {
--pf-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--link--Color: var(--ak-dark-foreground-link) !important;
}
.pf-c-radio {
--pf-c-radio__label--Color: var(--ak-dark-foreground);
}

View File

@ -1,7 +1,7 @@
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";
@ -19,15 +19,7 @@ export class Expand extends AKElement {
textClosed = msg("Show more");
static get styles(): CSSResult[] {
return [
PFBase,
PFExpandableSection,
css`
.pf-c-expandable-section.pf-m-display-lg {
background-color: var(--pf-global--BackgroundColor--100);
}
`,
];
return [PFBase, PFExpandableSection];
}
render(): TemplateResult {

View File

@ -69,12 +69,9 @@ export class PageHeader extends AKElement {
min-height: 114px;
}
.pf-c-button.pf-m-plain {
background-color: transparent;
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
border-radius: 0px;
}
.pf-c-page__main-section.pf-m-light {
background-color: transparent;
}
.pf-c-page__main-section {
flex-grow: 1;
display: flex;
@ -91,11 +88,6 @@ export class PageHeader extends AKElement {
.notification-trigger.has-notifications {
color: var(--pf-global--active-color--100);
}
h1 {
display: flex;
flex-direction: row;
align-items: center !important;
}
`,
];
}
@ -125,10 +117,10 @@ export class PageHeader extends AKElement {
renderIcon(): TemplateResult {
if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />&nbsp;`;
}
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>`;
return html`<i class=${icon}></i>&nbsp;`;
}
return html``;
}
@ -152,8 +144,8 @@ export class PageHeader extends AKElement {
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<slot name="icon">${this.renderIcon()}</slot>&nbsp;
<slot name="header">${this.header}</slot>
${this.renderIcon()}
<slot name="header"> ${this.header} </slot>
</h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>

View File

@ -27,21 +27,20 @@ const metadata: Meta<AKActionButton> = {
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
const container = (testItem: TemplateResult) => html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayMessage = (result: any) => {
@ -82,9 +81,7 @@ export const ButtonWithError = () => {
}, 3000);
});
return container(
html` <ak-action-button class="pf-m-secondary" .apiRequest=${run}
>3 Seconds</ak-action-button
>`,
);
return container(html` <ak-action-button class="pf-m-secondary" .apiRequest=${run}
>3 Seconds</ak-action-button
>`);
};

View File

@ -21,26 +21,25 @@ function makeid(length: number) {
// We want the display to be rich and comprehensive. The next two functions provide
// a styled wrapper for the return messages, and a styled wrapper for each message.
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
display: block;
margin-top: 1em;
}
p + p {
margin-top: 0.2em;
padding-left: 2.5rem;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
const container = (testItem: TemplateResult) => html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
display: block;
margin-top: 1em;
}
p + p {
margin-top: 0.2em;
padding-left: 2.5rem;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayMessage = (result: any) => {

View File

@ -33,12 +33,7 @@ export const FONT_COLOUR_DARK_MODE = "#fafafa";
export const FONT_COLOUR_LIGHT_MODE = "#151515";
export class RGBAColor {
constructor(
public r: number,
public g: number,
public b: number,
public a: number = 1,
) {}
constructor(public r: number, public g: number, public b: number, public a: number = 1) {}
toString(): string {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
}

View File

@ -97,10 +97,8 @@ export class ObjectChangelog extends Table<Event> {
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
return super.renderEmpty(html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`);
}
}

View File

@ -71,10 +71,8 @@ export class UserEvents extends Table<Event> {
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`,
);
return super.renderEmpty(html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>
</ak-empty-state>`);
}
}

View File

@ -22,10 +22,7 @@ import { ResponseError, ValidationError, ValidationErrorFromJSON } from "@goauth
export class PreventFormSubmit {
// Stub class which can be returned by form elements to prevent the form from submitting
constructor(
public message: string,
public element?: HorizontalFormElement,
) {}
constructor(public message: string, public element?: HorizontalFormElement) {}
}
export class APIError extends Error {

View File

@ -28,29 +28,31 @@ export class NotificationDrawer extends AKElement {
unread = 0;
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFNotificationDrawer, PFContent, PFDropdown].concat(css`
.pf-c-drawer__body {
height: 100%;
}
.pf-c-notification-drawer__body {
flex-grow: 1;
}
.pf-c-notification-drawer__header {
height: 114px;
align-items: center;
}
.pf-c-notification-drawer__header-action,
.pf-c-notification-drawer__header-action-close,
.pf-c-notification-drawer__header-action-close > .pf-c-button.pf-m-plain {
height: 100%;
}
.pf-c-notification-drawer__list-item-description {
white-space: pre-wrap;
}
.pf-c-notification-drawer__footer {
margin: 1rem;
}
`);
return [PFBase, PFButton, PFNotificationDrawer, PFContent, PFDropdown].concat(
css`
.pf-c-drawer__body {
height: 100%;
}
.pf-c-notification-drawer__body {
flex-grow: 1;
}
.pf-c-notification-drawer__header {
height: 114px;
align-items: center;
}
.pf-c-notification-drawer__header-action,
.pf-c-notification-drawer__header-action-close,
.pf-c-notification-drawer__header-action-close > .pf-c-button.pf-m-plain {
height: 100%;
}
.pf-c-notification-drawer__list-item-description {
white-space: pre-wrap;
}
.pf-c-notification-drawer__footer {
margin: 1rem;
}
`,
);
}
firstUpdated(): void {

View File

@ -48,9 +48,6 @@ export class TablePagination extends AKElement {
}
render(): TemplateResult {
if (!this.pages) {
return html``;
}
return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-options-menu">

View File

@ -44,22 +44,24 @@ export class IdentificationStage extends BaseStage<
form?: HTMLFormElement;
static get styles(): CSSResult[] {
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
/* login page's icons */
.pf-c-login__main-footer-links-item button {
background-color: transparent;
border: 0;
display: flex;
align-items: stretch;
}
.pf-c-login__main-footer-links-item img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100px;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`);
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(
css`
/* login page's icons */
.pf-c-login__main-footer-links-item button {
background-color: transparent;
border: 0;
display: flex;
align-items: stretch;
}
.pf-c-login__main-footer-links-item img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100px;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`,
);
}
firstUpdated(): void {

View File

@ -200,13 +200,12 @@ ${prompt.initialValue}</textarea
? LOCALES
: LOCALES.filter((locale) => locale.code !== "debug");
const options = locales.map(
(locale) =>
html`<option
value=${locale.code}
?selected=${locale.code === prompt.initialValue}
>
${locale.code.toUpperCase()} - ${locale.label()}
</option> `,
(locale) => html`<option
value=${locale.code}
?selected=${locale.code === prompt.initialValue}
>
${locale.code.toUpperCase()} - ${locale.label()}
</option> `,
);
return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">

View File

@ -1,8 +1,5 @@
import { PFSize } from "@goauthentik/app/elements/Spinner";
import { truncateWords } from "@goauthentik/common/utils";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand";
import "@goauthentik/user/LibraryApplication/AppIcon";
import { UserInterface } from "@goauthentik/user/UserInterface";
import { msg } from "@lit/localize";
@ -10,6 +7,7 @@ import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -32,36 +30,49 @@ export class LibraryApplication extends AKElement {
PFBase,
PFCard,
PFButton,
PFAvatar,
css`
.pf-c-card {
--pf-c-card--BoxShadow: var(--pf-global--BoxShadow--md);
height: 100%;
}
i.pf-icon {
height: 36px;
display: flex;
flex-direction: column;
justify-content: center;
}
.pf-c-avatar {
--pf-c-avatar--BorderRadius: 0;
}
.pf-c-card__header {
min-height: 60px;
justify-content: space-between;
flex-direction: column;
}
.pf-c-card__header a {
display: flex;
flex-direction: column;
justify-content: center;
}
a:hover {
text-decoration: none;
}
.expander {
flex-grow: 1;
}
.pf-c-card__title {
text-align: center;
/* This is not ideal as it hard limits us to 2 lines of text for the title
of the application. In theory that should be fine for most cases, but ideally
we don't do this */
height: 48px;
margin-right: 0.25em;
}
`,
];
}
renderIcon(): TemplateResult {
if (this.application?.metaIcon) {
if (this.application.metaIcon.startsWith("fa://")) {
const icon = this.application.metaIcon.replaceAll("fa://", "");
return html`<i class="fas ${icon}"></i>`;
}
return html`<img
class="app-icon pf-c-avatar"
src="${ifDefined(this.application.metaIcon)}"
alt="${msg("Application Icon")}"
/>`;
}
return html`<i class="fas fa-share-square"></i>`;
}
render(): TemplateResult {
if (!this.application) {
return html`<ak-spinner></ak-spinner>`;
@ -71,40 +82,41 @@ export class LibraryApplication extends AKElement {
class="pf-c-card pf-m-hoverable pf-m-compact ${this.selected
? "pf-m-selectable pf-m-selected"
: ""}"
style=${this.background !== "" ? `background: ${this.background} !important` : ""}
style="background: ${this.background} !important"
>
<div class="pf-c-card__header">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>
<ak-app-icon size=${PFSize.Large} .app=${this.application}></ak-app-icon>
${this.renderIcon()}
</a>
</div>
<div class="pf-c-card__title">
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>${this.application.name}</a
>
</div>
<div class="expander"></div>
<ak-expand textOpen=${msg("Less details")} textClosed=${msg("More details")}>
<div class="pf-c-content">
<small>${this.application.metaPublisher}</small>
</div>
${truncateWords(this.application.metaDescription || "", 10)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser
? html`
<a
class="pf-c-button pf-m-control pf-m-small pf-m-block"
class="pf-c-button pf-m-control pf-m-small"
href="/if/admin/#/core/applications/${this.application?.slug}"
>
<i class="fas fa-pencil-alt"></i>&nbsp;${msg("Edit")}
<i class="fas fa-pencil-alt"></i>
</a>
`
: html``}
</ak-expand>
</div>
<div class="pf-c-card__title">
<p>
<a
href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
>${this.application.name}</a
>
</p>
<div class="pf-c-content">
<small>${this.application.metaPublisher}</small>
</div>
</div>
<div class="pf-c-card__body">
${truncateWords(this.application.metaDescription || "", 35)}
</div>
</div>`;
}
}

View File

@ -1,80 +0,0 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { PFSize } from "@goauthentik/app/elements/Spinner";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import { Application } from "@goauthentik/api";
@customElement("ak-app-icon")
export class AppIcon extends AKElement {
@property({ attribute: false })
app?: Application;
@property()
size?: PFSize;
static get styles(): CSSResult[] {
return [
PFFAIcons,
PFAvatar,
css`
:host([size="pf-m-lg"]) {
--icon-height: 4rem;
--icon-border: 0.25rem;
}
:host([size="pf-m-md"]) {
--icon-height: 2rem;
--icon-border: 0.125rem;
}
:host([size="pf-m-sm"]) {
--icon-height: 1rem;
--icon-border: 0.125rem;
}
.pf-c-avatar {
--pf-c-avatar--BorderRadius: 0;
--pf-c-avatar--Height: calc(
var(--icon-height) + var(--icon-border) + var(--icon-border)
);
--pf-c-avatar--Width: calc(
var(--icon-height) + var(--icon-border) + var(--icon-border)
);
}
.icon {
font-size: var(--icon-height);
color: var(--ak-global--Color--100);
padding: var(--icon-border);
max-height: calc(var(--icon-height) + var(--icon-border) + var(--icon-border));
line-height: calc(var(--icon-height) + var(--icon-border) + var(--icon-border));
filter: drop-shadow(5px 5px 5px rgba(128, 128, 128, 0.25));
}
div {
height: calc(var(--icon-height) + var(--icon-border) + var(--icon-border));
}
`,
];
}
render(): TemplateResult {
if (!this.app) {
return html`<div><i class="icon fas fa-question-circle"></i></div>`;
}
if (this.app?.metaIcon) {
if (this.app.metaIcon.startsWith("fa://")) {
const icon = this.app.metaIcon.replaceAll("fa://", "");
return html`<div><i class="icon fas ${icon}"></i></div>`;
}
return html`<img
class="icon pf-c-avatar"
src="${ifDefined(this.app.metaIcon)}"
alt="${msg("Application Icon")}"
/>`;
}
return html`<span class="icon">${this.app?.name.charAt(0).toUpperCase()}</span>`;
}
}

View File

@ -73,22 +73,12 @@ export class UserInterface extends Interface {
z-index: auto !important;
background-color: transparent !important;
}
.pf-c-page__header {
background-color: transparent !important;
box-shadow: none !important;
color: black !important;
}
:host([theme="dark"]) .pf-c-page__header {
color: var(--ak-dark-foreground) !important;
}
.pf-c-page__header-tools-item .fas,
.pf-c-notification-badge__count,
.pf-c-page__header-tools-group .pf-c-button {
color: var(--ak-global--Color--100) !important;
}
.pf-c-page {
background-color: transparent;
}
.background-wrapper {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
.display-none {
display: none;
}
@ -102,19 +92,10 @@ export class UserInterface extends Interface {
.background-wrapper {
height: 100vh;
width: 100%;
position: fixed;
position: absolute;
z-index: -1;
top: 0;
left: 0;
background-color: var(--pf-c-page--BackgroundColor) !important;
}
.background-default-slant {
background-color: white; /*var(--ak-accent);*/
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 5vw));
height: 50vh;
}
:host([theme="dark"]) .background-default-slant {
background-color: black;
}
ak-locale-context {
display: flex;
@ -177,11 +158,7 @@ export class UserInterface extends Interface {
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}">
${this.uiConfig.theme.background === ""
? html`<div class="background-default-slant"></div>`
: html``}
</div>
<div class="background-wrapper" style="${this.uiConfig.theme.background}"></div>
<header class="pf-c-page__header">
<div class="pf-c-page__header-brand">
<a href="#/" class="pf-c-page__header-brand-link">
@ -238,7 +215,7 @@ export class UserInterface extends Interface {
? "pf-m-unread"
: ""}"
>
<i class="fas fa-bell" aria-hidden="true"></i>
<i class="pf-icon-bell" aria-hidden="true"></i>
<span class="pf-c-notification-badge__count"
>${this.notificationsCount}</span
>
@ -267,7 +244,7 @@ export class UserInterface extends Interface {
</div>
${this.me.user.isSuperuser
? html`<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
class="pf-c-button pf-m-primary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="/if/admin"
>
${msg("Admin interface")}

View File

@ -5751,6 +5751,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5766,18 +5775,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5789,51 +5825,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -6067,6 +6067,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -6082,18 +6091,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -6105,51 +6141,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -5659,6 +5659,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5674,18 +5683,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5697,51 +5733,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -5766,6 +5766,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5781,18 +5790,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5804,51 +5840,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -5898,6 +5898,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5913,18 +5922,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5936,51 +5972,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -6002,6 +6002,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -6017,18 +6026,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -6040,51 +6076,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -5649,6 +5649,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5664,18 +5673,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5687,51 +5723,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@ -618,9 +618,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>"。</target>
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@ -1072,8 +1072,8 @@
</trans-unit>
<trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="s55787f4dfcdce52b">
@ -1819,8 +1819,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@ -3248,8 +3248,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit>
<trans-unit id="s026555347e589f0e">
@ -4046,8 +4046,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target>
</trans-unit>
<trans-unit id="s44536d20bb5c8257">
@ -4056,8 +4056,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s3bb51cabb02b997e">
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
<source>Format: &quot;weeks=3;days=2;hours=3,seconds=2&quot;.</source>
<target>格式:&quot;weeks=3;days=2;hours=3,seconds=2&quot;。</target>
</trans-unit>
<trans-unit id="s04bfd02201db5ab8">
@ -4253,10 +4253,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@ -5372,7 +5372,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A "roaming" authenticator, like a YubiKey</source>
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@ -5712,10 +5712,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target>
<x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
<x id="0" equiv-text="${prompt.name}"/>&quot;
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为
<x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit>
@ -5764,7 +5764,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@ -7585,6 +7585,18 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>User type</source>
<target>用户类型</target>
</trans-unit>
<trans-unit id="s0b9a40b7b2853c7d">
<source>Default user</source>
<target>默认用户</target>
</trans-unit>
<trans-unit id="s35b9fa270f45b391">
<source>External user</source>
<target>外部用户</target>
</trans-unit>
<trans-unit id="s1a635369edaf4dc3">
<source>Service account</source>
<target>服务账户</target>
</trans-unit>
<trans-unit id="s0e427111d750cc02">
<source>Successfully updated license.</source>
<target>已成功更新许可证。</target>
@ -7605,6 +7617,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Licenses</source>
<target>许可证</target>
</trans-unit>
<trans-unit id="sf8f9f3032e891e16">
<source>TODO Copy</source>
<target>未定副本</target>
</trans-unit>
<trans-unit id="sd49099e9522635f4">
<source>License(s)</source>
<target>许可证</target>
@ -7613,6 +7629,34 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Enterprise is in preview.</source>
<target>企业版目前处于预览状态。</target>
</trans-unit>
<trans-unit id="s34dca481f039c226">
<source>How to get a license</source>
<target>如何获取许可证</target>
</trans-unit>
<trans-unit id="s948364901c166232">
<source>Copy the installation ID</source>
<target>复制安装 ID</target>
</trans-unit>
<trans-unit id="s75c167446b237e0f">
<source>Then open the customer portal</source>
<target>然后打开消费者中心</target>
</trans-unit>
<trans-unit id="s9748dd3bd53d27a4">
<source>Forecasted default users</source>
<target>预测默认用户</target>
</trans-unit>
<trans-unit id="s6b18f594d94c2374">
<source>Estimated user count one year from now</source>
<target>预计从此时开始一年内的用户数</target>
</trans-unit>
<trans-unit id="s69f246d164be88d0">
<source>Forecasted external users</source>
<target>预测外部用户</target>
</trans-unit>
<trans-unit id="s878fc2eaf94642db">
<source>Estimated external user count one year from now</source>
<target>预计从此时开始一年内的外部用户数</target>
</trans-unit>
<trans-unit id="sd22bd01bdf28c548">
<source>Cumulative license expiry</source>
<target>累计许可证过期时间</target>
@ -7621,6 +7665,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Update License</source>
<target>更新许可证</target>
</trans-unit>
<trans-unit id="s99afa741c259d70e">
<source>Create License</source>
<target>创建许可证</target>
</trans-unit>
<trans-unit id="s7df5b92a3f93544f">
<source>Warning: The current user count has exceeded the configured licenses.</source>
<target>警告:当前用户数超过了配置的许可证限制</target>
@ -7636,62 +7684,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
<target>管理企业版许可证</target>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
<target>未找到许可证。</target>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
<target>给我们发送反馈!</target>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
<target>获取许可证</target>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
<target>前往客户中心</target>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
<target>预测内部用户</target>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
<target>预测外部用户</target>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.externalUsers}"/> 名外部用户和 <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> 名预测的外部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
<target>安装</target>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
<target>安装许可证</target>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@ -5704,6 +5704,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5719,18 +5728,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5742,51 +5778,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -7617,6 +7617,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Licenses</source>
<target>许可证</target>
</trans-unit>
<trans-unit id="sf8f9f3032e891e16">
<source>TODO Copy</source>
<target>未定副本</target>
</trans-unit>
<trans-unit id="sd49099e9522635f4">
<source>License(s)</source>
<target>许可证</target>
@ -7625,6 +7629,34 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Enterprise is in preview.</source>
<target>企业版目前处于预览状态。</target>
</trans-unit>
<trans-unit id="s34dca481f039c226">
<source>How to get a license</source>
<target>如何获取许可证</target>
</trans-unit>
<trans-unit id="s948364901c166232">
<source>Copy the installation ID</source>
<target>复制安装 ID</target>
</trans-unit>
<trans-unit id="s75c167446b237e0f">
<source>Then open the customer portal</source>
<target>然后打开消费者中心</target>
</trans-unit>
<trans-unit id="s9748dd3bd53d27a4">
<source>Forecasted default users</source>
<target>预测默认用户</target>
</trans-unit>
<trans-unit id="s6b18f594d94c2374">
<source>Estimated user count one year from now</source>
<target>预计从此时开始一年内的用户数</target>
</trans-unit>
<trans-unit id="s69f246d164be88d0">
<source>Forecasted external users</source>
<target>预测外部用户</target>
</trans-unit>
<trans-unit id="s878fc2eaf94642db">
<source>Estimated external user count one year from now</source>
<target>预计从此时开始一年内的外部用户数</target>
</trans-unit>
<trans-unit id="sd22bd01bdf28c548">
<source>Cumulative license expiry</source>
<target>累计许可证过期时间</target>
@ -7633,6 +7665,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Update License</source>
<target>更新许可证</target>
</trans-unit>
<trans-unit id="s99afa741c259d70e">
<source>Create License</source>
<target>创建许可证</target>
</trans-unit>
<trans-unit id="s7df5b92a3f93544f">
<source>Warning: The current user count has exceeded the configured licenses.</source>
<target>警告:当前用户数超过了配置的许可证限制</target>
@ -7648,46 +7684,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
<target>管理企业版许可证</target>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
<target>未找到许可证。</target>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
<target>给我们发送反馈!</target>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
<target>获取许可证</target>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
<target>前往客户中心</target>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
<target>预测内部用户</target>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
<target>预测外部用户</target>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.externalUsers}"/> 名外部用户和 <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> 名预测的外部用户,估算从此时起一年后的用户数。</target>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
<target>安装</target>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
<target>安装许可证</target>
</trans-unit>
</body>
</file>

View File

@ -5703,6 +5703,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5718,18 +5727,45 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
@ -5741,51 +5777,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s9ce7cc01fb9b5b53">
<source>Manage enterprise licenses</source>
</trans-unit>
<trans-unit id="sf9ebf11ac2645820">
<source>No licenses found.</source>
</trans-unit>
<trans-unit id="sa1db89262360550b">
<source>Send us feedback!</source>
</trans-unit>
<trans-unit id="s4015746f55a8d89f">
<source>Get a license</source>
</trans-unit>
<trans-unit id="sb2cbd06f8e25b47e">
<source>Go to Customer Portal</source>
</trans-unit>
<trans-unit id="sf58825457d61c429">
<source>Forecast internal users</source>
</trans-unit>
<trans-unit id="sde9a3f41977ec1f8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
</trans-unit>
<trans-unit id="sf52479d6daa0a4a8">
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.externalUsers}"/> current external users and <x id="1" equiv-text="${this.forecast?.forecastedExternalUsers}"/> forecasted external users.</source>
</trans-unit>
<trans-unit id="s6196153c4b0c1ea0">
<source>Install</source>
</trans-unit>
<trans-unit id="s0285b4bd69130fa3">
<source>Install License</source>
</trans-unit>
<trans-unit id="scef2eb6a2bfe3110">
<source>Internal users might be users such as company employees, which will get access to the full Enterprise feature set.</source>
</trans-unit>
<trans-unit id="sf66389b04fcc219c">
<source>External users might be external consultants or B2C customers. These users don't get access to enterprise features.</source>
</trans-unit>
<trans-unit id="s77e8668a27dbc402">
<source>Service accounts should be used for machine-to-machine authentication or other automations.</source>
</trans-unit>
<trans-unit id="s28cbd874ba450b4e">
<source>Less details</source>
</trans-unit>
<trans-unit id="s8fa26f65aed77c96">
<source>More details</source>
</trans-unit>
</body>
</file>

View File

@ -1,37 +0,0 @@
---
title: "Procedural topic"
---
Use a title that focuses on the task you are writing about... for example, "Add a new Group" or "Edit user profiles". For procedural docs, there should be a verb in the tilte, and usually the noun (the thing you are working on). For the title (and all headings) use the infinitive form of the verb (i.e. "add") not the gerund form (i.e. "adding").
In this first section write one or two sentences about the task. Keep it brief; if it goes on too long, then create a separate conceptual topic, in a separate `.md` file. We don't want readers to have to scroll through paragraphs of conceptual info before they get to Step 1.
## Prerequisites (optional section)
In this section, inform the reader of anything they need to do, or have configured or installed, before they start following the procedural instructions below.
## Overview of steps/workflow (optional section)
If the task is quite long or complex, it might be good to add a bullet list of the main steps, or even a diagram of the workflow, just so that the reader can first familairize themselves with the 50,000 meter view before they dive into the detailed steps.
## first several group steps
If the task involves a lot of steps, try to group them into simalr steps and have a Head3 or Hedad4 title for each group.
In this section, help the reader get oriented... where do they need to be (i.e. in the GUI, on a CLI, etc).
Have a separate paragraph for each step.
Start instructions with the desired outcome, followed by the instructions.
EXAMPLE: To define a new port number, navigate to the Admin interface, and then to the **Settings** tab.
## next step of grouped steps
Continue with the steps...
Use screenshots sparingly, only for complex UIs where it is difficult to describe a UI element with words.
## verify the steps
Whenever possible, it is useful to add verification steps at the end of a procedural topic. For example, if the procedural was about installing a product, use this section to tell them how they can verify that the install was successful

View File

@ -10,16 +10,16 @@ Everyone welcome; we will work on code, docs, and anything else that looks inter
Moderators will be available for most US and European hours, so if during the multi-day event, participants have questions or a PR needs a technical review, we are here for you.
Prizes? Why, Yes! We've got a total prize pool of $5000 and a bunch of cool authentik-branded socks and, indubitably, GitHub fame.
Prizes? Why, Yes! We've got a total prize pool of $5000 and a bunch of cool authentik-branded socks, food certificates, and indubitably GitHub fame.
## When
July 26-30, 2023
- Kickoff meeting is on Wednesday, July 26th, at 8:00am Pacific USA (UTC -7), 5:00pm in Central Europe (UTC +2), and 8:30pm in Mumbai (UTC +5.30)
- Check-in calls on Thursday and Friday, for one hour, at the same times as above.
- Wrap-up and first demos on Saturday, starting at same times as above.
- Final demos, voting, and awards on Sunday! Yep, same times as above.
* Kickoff meeting is on Wednesday, July 26th, at 8:00am Pacific USA (UTC -7), 5:00pm in Central Europe (UTC +2), and 8:30pm in Mumbai (UTC +5.30)
* Check-in calls on Thursday and Friday, for one hour, at the same times as above.
* Wrap-up and first demos on Saturday, starting at same times as above.
* Final demos, voting, and awards on Sunday! Yep, same times as above.
## Where
@ -34,9 +34,8 @@ Use the [Hackathon 2023 Registration Form](https://docs.google.com/forms/d/e/1FA
If you already know what you and/or your team want to work on, you can open an [Issue](https://github.com/goauthentik/authentik/issues) using our template for all hackathon Issues at any time (why not now?) and add the `hackathon` label. Then, when you register, enter the Issue number that you opened on your registration form. This way, on Kickoff Day we can easily match particpants with their Issue of interest.
During the Kickoff call, there will be time to peruse existing Issues and add emotes to indicate your interest in working on it (or having it worked on!)
- 🚀 I want to work on this
- ❤️ I want to see this worked on
- 🚀 I want to work on this
- ❤️ I want to see this worked on
## Agenda
@ -52,7 +51,7 @@ During the Kickoff call, there will be time to peruse existing Issues and add em
## About that money...
Be aware that all prize money distributions will follow local/state/country laws regarding taxation, not providing funds to citizens of countries prohibited by US law, and all other legal requirements.
Be aware that all prize money distributions will follow local/state/country laws regarding taxation, providing funds to countries prohibited by US law, and all other legal requirements.
## Questions?

Some files were not shown because too many files have changed in this diff Show More