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", "name",
"key", "key",
"expiry", "expiry",
"users", "internal_users",
"external_users", "external_users",
] ]
extra_kwargs = { extra_kwargs = {
"name": {"read_only": True}, "name": {"read_only": True},
"expiry": {"read_only": True}, "expiry": {"read_only": True},
"users": {"read_only": True}, "internal_users": {"read_only": True},
"external_users": {"read_only": True}, "external_users": {"read_only": True},
} }
@ -49,7 +49,7 @@ class LicenseSerializer(ModelSerializer):
class LicenseSummary(PassiveSerializer): class LicenseSummary(PassiveSerializer):
"""Serializer for license status""" """Serializer for license status"""
users = IntegerField(required=True) internal_users = IntegerField(required=True)
external_users = IntegerField(required=True) external_users = IntegerField(required=True)
valid = BooleanField() valid = BooleanField()
show_admin_warning = BooleanField() show_admin_warning = BooleanField()
@ -62,9 +62,9 @@ class LicenseSummary(PassiveSerializer):
class LicenseForecastSerializer(PassiveSerializer): class LicenseForecastSerializer(PassiveSerializer):
"""Serializer for license forecast""" """Serializer for license forecast"""
users = IntegerField(required=True) internal_users = IntegerField(required=True)
external_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) forecasted_external_users = IntegerField(required=True)
@ -111,7 +111,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
latest_valid = datetime.fromtimestamp(total.exp) latest_valid = datetime.fromtimestamp(total.exp)
response = LicenseSummary( response = LicenseSummary(
data={ data={
"users": total.users, "internal_users": total.internal_users,
"external_users": total.external_users, "external_users": total.external_users,
"valid": total.is_valid(), "valid": total.is_valid(),
"show_admin_warning": show_admin_warning, "show_admin_warning": show_admin_warning,
@ -135,8 +135,8 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
def forecast(self, request: Request) -> Response: def forecast(self, request: Request) -> Response:
"""Forecast how many users will be required in a year""" """Forecast how many users will be required in a year"""
last_month = now() - timedelta(days=30) last_month = now() - timedelta(days=30)
# Forecast for default users # Forecast for internal users
users_in_last_month = User.objects.filter( internal_in_last_month = User.objects.filter(
type=UserTypes.INTERNAL, date_joined__gte=last_month type=UserTypes.INTERNAL, date_joined__gte=last_month
).count() ).count()
# Forecast for external users # Forecast for external users
@ -144,9 +144,9 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
forecast_for_months = 12 forecast_for_months = 12
response = LicenseForecastSerializer( response = LicenseForecastSerializer(
data={ data={
"users": LicenseKey.get_default_user_count(), "internal_users": LicenseKey.get_default_user_count(),
"external_users": LicenseKey.get_external_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), "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.exceptions import InvalidSignature
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
from dacite import from_dict from dacite import from_dict
from django.contrib.postgres.indexes import HashIndex
from django.db import models from django.db import models
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils.timezone import now from django.utils.timezone import now
@ -46,7 +47,7 @@ class LicenseKey:
exp: int exp: int
name: str name: str
users: int internal_users: int
external_users: int external_users: int
flags: list[LicenseFlags] = field(default_factory=list) flags: list[LicenseFlags] = field(default_factory=list)
@ -87,7 +88,7 @@ class LicenseKey:
active_licenses = License.objects.filter(expiry__gte=now()) active_licenses = License.objects.filter(expiry__gte=now())
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0) total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in active_licenses: for lic in active_licenses:
total.users += lic.users total.internal_users += lic.internal_users
total.external_users += lic.external_users total.external_users += lic.external_users
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
@ -123,7 +124,7 @@ class LicenseKey:
Only checks the current count, no historical data is checked""" Only checks the current count, no historical data is checked"""
default_users = self.get_default_user_count() default_users = self.get_default_user_count()
if default_users > self.users: if default_users > self.internal_users:
return False return False
active_users = self.get_external_user_count() active_users = self.get_external_user_count()
if active_users > self.external_users: if active_users > self.external_users:
@ -153,11 +154,11 @@ class License(models.Model):
"""An authentik enterprise license""" """An authentik enterprise license"""
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
key = models.TextField(unique=True) key = models.TextField()
name = models.TextField() name = models.TextField()
expiry = models.DateTimeField() expiry = models.DateTimeField()
users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property @property
@ -165,6 +166,9 @@ class License(models.Model):
"""Get parsed license status""" """Get parsed license status"""
return LicenseKey.validate(self.key) return LicenseKey.validate(self.key)
class Meta:
indexes = (HashIndex(fields=("key",)),)
def usage_expiry(): def usage_expiry():
"""Keep license usage records for 3 months""" """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""" """Extract data from license jwt and save it into model"""
status = instance.status status = instance.status
instance.name = status.name instance.name = status.name
instance.users = status.users instance.internal_users = status.internal_users
instance.external_users = status.external_users instance.external_users = status.external_users
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone()) instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())

View File

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

View File

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

View File

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

View File

@ -170,11 +170,11 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
icon="pf-icon pf-icon-user" icon="pf-icon pf-icon-user"
header=${msg("Forecast internal users")} header=${msg("Forecast internal users")}
subtext=${msg( 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) + ~&nbsp;${(this.forecast?.internalUsers || 0) +
(this.forecast?.forecastedUsers || 0)} (this.forecast?.forecastedInternalUsers || 0)}
</ak-aggregate-card> </ak-aggregate-card>
<ak-aggregate-card <ak-aggregate-card
class="pf-l-grid__item" class="pf-l-grid__item"
@ -217,10 +217,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
} }
return [ return [
html`<div>${item.name}</div>`, html`<div>${item.name}</div>`,
html`<div> html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
<small>0 / ${item.users}</small> <div>${msg(str`External: ${item.externalUsers}`)}</div>`,
<small>0 / ${item.externalUsers}</small>
</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`, html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </file>

View File

@ -7658,8 +7658,8 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>预测内部用户</target> <target>预测内部用户</target>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target> <target>根据当前 <x id="0" equiv-text="${this.forecast?.internalUsers}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
</trans-unit> </trans-unit>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <source>Forecast external users</source>
@ -7765,6 +7765,12 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5759,7 +5759,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Forecast internal users</source> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </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> <source>Forecast internal users</source>
</trans-unit> </trans-unit>
<trans-unit id="sde9a3f41977ec1f8"> <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>
<trans-unit id="s4557b6b9da258643"> <trans-unit id="s4557b6b9da258643">
<source>Forecast external users</source> <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>
<trans-unit id="s6931695c4f563bc4"> <trans-unit id="s6931695c4f563bc4">
<source>The length of the individual generated tokens. Can be increased to improve security.</source> <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> </trans-unit>
</body> </body>
</file> </file>