enterprise: licensing fixes (#6601)

* enterprise: fix unique index for key, fix field names

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* enterprise: update UI to match

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-08-23 13:20:42 +02:00 committed by GitHub
parent b93d1cd008
commit 168423a54e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1684 additions and 1593 deletions

View File

@ -35,13 +35,13 @@ class LicenseSerializer(ModelSerializer):
"name",
"key",
"expiry",
"users",
"internal_users",
"external_users",
]
extra_kwargs = {
"name": {"read_only": True},
"expiry": {"read_only": True},
"users": {"read_only": True},
"internal_users": {"read_only": True},
"external_users": {"read_only": True},
}
@ -49,7 +49,7 @@ class LicenseSerializer(ModelSerializer):
class LicenseSummary(PassiveSerializer):
"""Serializer for license status"""
users = IntegerField(required=True)
internal_users = IntegerField(required=True)
external_users = IntegerField(required=True)
valid = BooleanField()
show_admin_warning = BooleanField()
@ -62,9 +62,9 @@ class LicenseSummary(PassiveSerializer):
class LicenseForecastSerializer(PassiveSerializer):
"""Serializer for license forecast"""
users = IntegerField(required=True)
internal_users = IntegerField(required=True)
external_users = IntegerField(required=True)
forecasted_users = IntegerField(required=True)
forecasted_internal_users = IntegerField(required=True)
forecasted_external_users = IntegerField(required=True)
@ -111,7 +111,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
latest_valid = datetime.fromtimestamp(total.exp)
response = LicenseSummary(
data={
"users": total.users,
"internal_users": total.internal_users,
"external_users": total.external_users,
"valid": total.is_valid(),
"show_admin_warning": show_admin_warning,
@ -135,8 +135,8 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
def forecast(self, request: Request) -> Response:
"""Forecast how many users will be required in a year"""
last_month = now() - timedelta(days=30)
# Forecast for default users
users_in_last_month = User.objects.filter(
# Forecast for internal users
internal_in_last_month = User.objects.filter(
type=UserTypes.INTERNAL, date_joined__gte=last_month
).count()
# Forecast for external users
@ -144,9 +144,9 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
forecast_for_months = 12
response = LicenseForecastSerializer(
data={
"users": LicenseKey.get_default_user_count(),
"internal_users": LicenseKey.get_default_user_count(),
"external_users": LicenseKey.get_external_user_count(),
"forecasted_users": (users_in_last_month * forecast_for_months),
"forecasted_internal_users": (internal_in_last_month * forecast_for_months),
"forecasted_external_users": (external_in_last_month * forecast_for_months),
}
)

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.4 on 2023-08-23 10:06
import django.contrib.postgres.indexes
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_enterprise", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name="license",
old_name="users",
new_name="internal_users",
),
migrations.AlterField(
model_name="license",
name="key",
field=models.TextField(),
),
migrations.AddIndex(
model_name="license",
index=django.contrib.postgres.indexes.HashIndex(
fields=["key"], name="authentik_e_key_523e13_hash"
),
),
]

View File

@ -11,6 +11,7 @@ from uuid import uuid4
from cryptography.exceptions import InvalidSignature
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
from dacite import from_dict
from django.contrib.postgres.indexes import HashIndex
from django.db import models
from django.db.models.query import QuerySet
from django.utils.timezone import now
@ -46,7 +47,7 @@ class LicenseKey:
exp: int
name: str
users: int
internal_users: int
external_users: int
flags: list[LicenseFlags] = field(default_factory=list)
@ -87,7 +88,7 @@ class LicenseKey:
active_licenses = License.objects.filter(expiry__gte=now())
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in active_licenses:
total.users += lic.users
total.internal_users += lic.internal_users
total.external_users += lic.external_users
exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0:
@ -123,7 +124,7 @@ class LicenseKey:
Only checks the current count, no historical data is checked"""
default_users = self.get_default_user_count()
if default_users > self.users:
if default_users > self.internal_users:
return False
active_users = self.get_external_user_count()
if active_users > self.external_users:
@ -153,11 +154,11 @@ class License(models.Model):
"""An authentik enterprise license"""
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
key = models.TextField(unique=True)
key = models.TextField()
name = models.TextField()
expiry = models.DateTimeField()
users = models.BigIntegerField()
internal_users = models.BigIntegerField()
external_users = models.BigIntegerField()
@property
@ -165,6 +166,9 @@ class License(models.Model):
"""Get parsed license status"""
return LicenseKey.validate(self.key)
class Meta:
indexes = (HashIndex(fields=("key",)),)
def usage_expiry():
"""Keep license usage records for 3 months"""

View File

@ -13,6 +13,6 @@ def pre_save_license(sender: type[License], instance: License, **_):
"""Extract data from license jwt and save it into model"""
status = instance.status
instance.name = status.name
instance.users = status.users
instance.internal_users = status.internal_users
instance.external_users = status.external_users
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())

View File

@ -23,7 +23,7 @@ class TestEnterpriseLicense(TestCase):
aud="",
exp=_exp,
name=generate_id(),
users=100,
internal_users=100,
external_users=100,
)
),
@ -32,7 +32,7 @@ class TestEnterpriseLicense(TestCase):
"""Check license verification"""
lic = License.objects.create(key=generate_id())
self.assertTrue(lic.status.is_valid())
self.assertEqual(lic.users, 100)
self.assertEqual(lic.internal_users, 100)
def test_invalid(self):
"""Test invalid license"""
@ -46,7 +46,7 @@ class TestEnterpriseLicense(TestCase):
aud="",
exp=_exp,
name=generate_id(),
users=100,
internal_users=100,
external_users=100,
)
),
@ -58,7 +58,7 @@ class TestEnterpriseLicense(TestCase):
lic2 = License.objects.create(key=generate_id())
self.assertTrue(lic2.status.is_valid())
total = LicenseKey.get_total()
self.assertEqual(total.users, 200)
self.assertEqual(total.internal_users, 200)
self.assertEqual(total.external_users, 200)
self.assertEqual(total.exp, _exp)
self.assertTrue(total.is_valid())

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-17 17:37+0000\n"
"POT-Creation-Date: 2023-08-23 10:04+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"
@ -98,125 +98,125 @@ msgstr ""
msgid "Users added to this group will be superusers."
msgstr ""
#: authentik/core/models.py:162
#: authentik/core/models.py:142
msgid "User's display name."
msgstr ""
#: authentik/core/models.py:256 authentik/providers/oauth2/models.py:294
#: authentik/core/models.py:268 authentik/providers/oauth2/models.py:294
msgid "User"
msgstr ""
#: authentik/core/models.py:257
#: authentik/core/models.py:269
msgid "Users"
msgstr ""
#: authentik/core/models.py:270
#: authentik/core/models.py:282
msgid ""
"Flow used for authentication when the associated application is accessed by "
"an un-authenticated user."
msgstr ""
#: authentik/core/models.py:280
#: authentik/core/models.py:292
msgid "Flow used when authorizing this provider."
msgstr ""
#: authentik/core/models.py:292
#: authentik/core/models.py:304
msgid ""
"Accessed from applications; optional backchannel providers for protocols "
"like LDAP and SCIM."
msgstr ""
#: authentik/core/models.py:347
#: authentik/core/models.py:359
msgid "Application's display Name."
msgstr ""
#: authentik/core/models.py:348
#: authentik/core/models.py:360
msgid "Internal application name, used in URLs."
msgstr ""
#: authentik/core/models.py:360
#: authentik/core/models.py:372
msgid "Open launch URL in a new browser tab or window."
msgstr ""
#: authentik/core/models.py:424
#: authentik/core/models.py:436
msgid "Application"
msgstr ""
#: authentik/core/models.py:425
#: authentik/core/models.py:437
msgid "Applications"
msgstr ""
#: authentik/core/models.py:431
#: authentik/core/models.py:443
msgid "Use the source-specific identifier"
msgstr ""
#: authentik/core/models.py:433
#: authentik/core/models.py:445
msgid ""
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
msgstr ""
#: authentik/core/models.py:437
#: authentik/core/models.py:449
msgid ""
"Use the user's email address, but deny enrollment when the email address "
"already exists."
msgstr ""
#: authentik/core/models.py:440
#: authentik/core/models.py:452
msgid ""
"Link to a user with identical username. Can have security implications when "
"a username is used with another source."
msgstr ""
#: authentik/core/models.py:444
#: authentik/core/models.py:456
msgid ""
"Use the user's username, but deny enrollment when the username already "
"exists."
msgstr ""
#: authentik/core/models.py:451
#: authentik/core/models.py:463
msgid "Source's display Name."
msgstr ""
#: authentik/core/models.py:452
#: authentik/core/models.py:464
msgid "Internal source name, used in URLs."
msgstr ""
#: authentik/core/models.py:471
#: authentik/core/models.py:483
msgid "Flow to use when authenticating existing users."
msgstr ""
#: authentik/core/models.py:480
#: authentik/core/models.py:492
msgid "Flow to use when enrolling new users."
msgstr ""
#: authentik/core/models.py:488
#: authentik/core/models.py:500
msgid ""
"How the source determines if an existing user should be authenticated or a "
"new user enrolled."
msgstr ""
#: authentik/core/models.py:660
#: authentik/core/models.py:672
msgid "Token"
msgstr ""
#: authentik/core/models.py:661
#: authentik/core/models.py:673
msgid "Tokens"
msgstr ""
#: authentik/core/models.py:702
#: authentik/core/models.py:714
msgid "Property Mapping"
msgstr ""
#: authentik/core/models.py:703
#: authentik/core/models.py:715
msgid "Property Mappings"
msgstr ""
#: authentik/core/models.py:738
#: authentik/core/models.py:750
msgid "Authenticated Session"
msgstr ""
#: authentik/core/models.py:739
#: authentik/core/models.py:751
msgid "Authenticated Sessions"
msgstr ""

View File

@ -31714,7 +31714,7 @@ components:
type: string
format: date-time
readOnly: true
users:
internal_users:
type: integer
readOnly: true
external_users:
@ -31723,27 +31723,27 @@ components:
required:
- expiry
- external_users
- internal_users
- key
- license_uuid
- name
- users
LicenseForecast:
type: object
description: Serializer for license forecast
properties:
users:
internal_users:
type: integer
external_users:
type: integer
forecasted_users:
forecasted_internal_users:
type: integer
forecasted_external_users:
type: integer
required:
- external_users
- forecasted_external_users
- forecasted_users
- users
- forecasted_internal_users
- internal_users
LicenseRequest:
type: object
description: License Serializer
@ -31757,7 +31757,7 @@ components:
type: object
description: Serializer for license status
properties:
users:
internal_users:
type: integer
external_users:
type: integer
@ -31777,11 +31777,11 @@ components:
required:
- external_users
- has_license
- internal_users
- latest_valid
- read_only
- show_admin_warning
- show_user_warning
- users
- valid
Link:
type: object

View File

@ -170,11 +170,11 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
icon="pf-icon pf-icon-user"
header=${msg("Forecast internal 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?.internalUsers} current internal users and ${this.forecast?.forecastedInternalUsers} forecasted internal users.`,
)}
>
~&nbsp;${(this.forecast?.users || 0) +
(this.forecast?.forecastedUsers || 0)}
~&nbsp;${(this.forecast?.internalUsers || 0) +
(this.forecast?.forecastedInternalUsers || 0)}
</ak-aggregate-card>
<ak-aggregate-card
class="pf-l-grid__item"
@ -217,10 +217,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
}
return [
html`<div>${item.name}</div>`,
html`<div>
<small>0 / ${item.users}</small>
<small>0 / ${item.externalUsers}</small>
</div>`,
html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
<div>${msg(str`External: ${item.externalUsers}`)}</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>

View File

@ -5806,7 +5806,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5888,6 +5888,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -6122,7 +6122,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6204,6 +6204,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -5714,7 +5714,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5796,6 +5796,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -5821,7 +5821,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5903,6 +5903,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -5953,7 +5953,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6035,6 +6035,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -6057,7 +6057,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -6139,6 +6139,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -5704,7 +5704,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5786,6 +5786,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

File diff suppressed because it is too large Load Diff

View File

@ -5759,7 +5759,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5841,6 +5841,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>

View File

@ -5758,7 +5758,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<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>
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
</trans-unit>
<trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source>
@ -5840,6 +5840,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
</trans-unit>
<trans-unit id="s0dd031b58ed4017c">
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
</trans-unit>
<trans-unit id="s57b07e524f8f5c2a">
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
</trans-unit>
</body>
</file>