diff --git a/.gitignore b/.gitignore index e1e5f32d7..3f398175f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ dump.rdb .cache/ .idea/ .vscode/ +.fleet/ db.sqlite3 config.py config.yml diff --git a/Dockerfile b/Dockerfile index f236c86eb..cf8f73a29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim as stage-build +FROM python:3.9-slim as stage-build ARG TARGETARCH ARG VERSION @@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver ADD . . RUN cd utils && bash -ixeu build.sh -FROM python:3.8-slim +FROM python:3.9-slim ARG TARGETARCH MAINTAINER JumpServer Team @@ -18,7 +18,6 @@ ARG BUILD_DEPENDENCIES=" \ pkg-config" ARG DEPENDENCIES=" \ - default-libmysqlclient-dev \ freetds-dev \ libpq-dev \ libffi-dev \ @@ -28,21 +27,20 @@ ARG DEPENDENCIES=" \ libxml2-dev \ libxmlsec1-dev \ libxmlsec1-openssl \ - libaio-dev \ - openssh-client \ - sshpass" + libaio-dev" ARG TOOLS=" \ ca-certificates \ curl \ + default-libmysqlclient-dev \ default-mysql-client \ - iputils-ping \ locales \ + openssh-client \ procps \ - redis-tools \ + sshpass \ telnet \ - vim \ unzip \ + vim \ wget" ARG APT_MIRROR=http://mirrors.ustc.edu.cn @@ -82,6 +80,8 @@ ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR +ARG DEBUG + RUN --mount=type=cache,target=/root/.cache/pip \ set -ex \ && pip config set global.index-url ${PIP_MIRROR} \ diff --git a/Dockerfile-ee b/Dockerfile-ee new file mode 100644 index 000000000..63c65d21c --- /dev/null +++ b/Dockerfile-ee @@ -0,0 +1,10 @@ +ARG VERSION +FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack +FROM jumpserver/core:${VERSION} +COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack + +WORKDIR /opt/jumpserver + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -ex \ + && pip install -r requirements/requirements_xpack.txt diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 index c2fa521b6..0ed0c08ad 100644 --- a/Dockerfile.loong64 +++ b/Dockerfile.loong64 @@ -1,4 +1,4 @@ -FROM python:3.8-slim as stage-build +FROM python:3.9-slim as stage-build ARG TARGETARCH ARG VERSION @@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver ADD . . RUN cd utils && bash -ixeu build.sh -FROM python:3.8-slim +FROM python:3.9-slim ARG TARGETARCH MAINTAINER JumpServer Team @@ -18,7 +18,6 @@ ARG BUILD_DEPENDENCIES=" \ pkg-config" ARG DEPENDENCIES=" \ - default-libmysqlclient-dev \ freetds-dev \ libpq-dev \ libffi-dev \ @@ -28,21 +27,20 @@ ARG DEPENDENCIES=" \ libxml2-dev \ libxmlsec1-dev \ libxmlsec1-openssl \ - libaio-dev \ - openssh-client \ - sshpass" + libaio-dev" ARG TOOLS=" \ ca-certificates \ curl \ + default-libmysqlclient-dev \ default-mysql-client \ - iputils-ping \ locales \ - netcat \ - redis-server \ + openssh-client \ + procps \ + sshpass \ telnet \ - vim \ unzip \ + vim \ wget" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ @@ -69,13 +67,15 @@ ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR +ARG DEBUG + RUN --mount=type=cache,target=/root/.cache/pip \ set -ex \ && pip config set global.index-url ${PIP_MIRROR} \ && pip install --upgrade pip \ && pip install --upgrade setuptools wheel \ - && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \ - && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \ + && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \ + && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \ && pip install $(grep 'PyNaCl' requirements/requirements.txt) \ && GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \ && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ diff --git a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py index 95b12e9f0..cc91c998e 100644 --- a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py +++ b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', max_length=16, verbose_name='Type')), ('content', models.TextField(help_text='One line one command', verbose_name='Content')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')), ], options={ @@ -57,7 +58,8 @@ class Migration(migrations.Migration): ('assets', models.JSONField(verbose_name='Asset')), ('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')), ( - 'reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), + 'reviewers', + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), ], options={ 'verbose_name': 'Command acl', diff --git a/apps/acls/migrations/0008_commandgroup_comment.py b/apps/acls/migrations/0008_commandgroup_comment.py index 631ff8eb7..82e44912c 100644 --- a/apps/acls/migrations/0008_commandgroup_comment.py +++ b/apps/acls/migrations/0008_commandgroup_comment.py @@ -1,18 +1,33 @@ # Generated by Django 3.2.14 on 2022-12-02 04:25 -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('acls', '0007_auto_20221202_1048'), ] operations = [ - migrations.AddField( - model_name='commandgroup', - name='comment', - field=models.TextField(blank=True, verbose_name='Comment'), + migrations.AlterModelOptions( + name='commandgroup', + options={'verbose_name': 'Command group'}, + ), + migrations.RenameField( + model_name='commandfilteracl', + old_name='commands', + new_name='command_groups', + ), + migrations.AlterModelOptions( + name='commandfilteracl', + options={'ordering': ('priority', 'name'), 'verbose_name': 'Command acl'}, + ), + migrations.AlterModelOptions( + name='loginacl', + options={'ordering': ('priority', 'name'), 'verbose_name': 'Login acl'}, + ), + migrations.AlterModelOptions( + name='loginassetacl', + options={'ordering': ('priority', 'name'), 'verbose_name': 'Login asset acl'}, ), ] diff --git a/apps/acls/migrations/0009_auto_20221204_0001.py b/apps/acls/migrations/0009_auto_20221204_0001.py deleted file mode 100644 index b5286160f..000000000 --- a/apps/acls/migrations/0009_auto_20221204_0001.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-03 16:01 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('acls', '0008_commandgroup_comment'), - ] - - operations = [ - migrations.AlterModelOptions( - name='commandgroup', - options={'verbose_name': 'Command group'}, - ), - migrations.RenameField( - model_name='commandfilteracl', - old_name='commands', - new_name='command_groups', - ), - ] diff --git a/apps/acls/migrations/0009_auto_20221220_1956.py b/apps/acls/migrations/0009_auto_20221220_1956.py new file mode 100644 index 000000000..78f8b1152 --- /dev/null +++ b/apps/acls/migrations/0009_auto_20221220_1956.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('acls', '0008_commandgroup_comment'), + ] + + operations = [ + migrations.AddField( + model_name='commandfilteracl', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='loginacl', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='loginassetacl', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='commandfilteracl', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='commandgroup', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='commandgroup', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='loginacl', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='loginassetacl', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/acls/migrations/0010_auto_20221205_1122.py b/apps/acls/migrations/0010_auto_20221205_1122.py deleted file mode 100644 index 78adde93b..000000000 --- a/apps/acls/migrations/0010_auto_20221205_1122.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 03:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('acls', '0009_auto_20221204_0001'), - ] - - operations = [ - migrations.AlterModelOptions( - name='commandfilteracl', - options={'ordering': ('priority', 'name'), 'verbose_name': 'Command acl'}, - ), - migrations.AlterModelOptions( - name='loginacl', - options={'ordering': ('priority', 'name'), 'verbose_name': 'Login acl'}, - ), - migrations.AlterModelOptions( - name='loginassetacl', - options={'ordering': ('priority', 'name'), 'verbose_name': 'Login asset acl'}, - ), - ] diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 704e3d743..256241361 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -3,7 +3,7 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from common.mixins import CommonModelMixin +from common.db.models import JMSBaseModel from common.utils import contains_ip from orgs.mixins.models import OrgModelMixin @@ -58,7 +58,7 @@ class UserAssetAccountACLQuerySet(BaseACLQuerySet): def filter_account(self, username): q = Q(accounts__username_group__contains=username) | \ - Q(accounts__username_group__contains='*') + Q(accounts__username_group__contains='*') return self.filter(q) @@ -67,7 +67,7 @@ class ACLManager(models.Manager): return self.get_queryset().valid() -class BaseACL(CommonModelMixin): +class BaseACL(JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) priority = models.IntegerField( default=50, verbose_name=_("Priority"), @@ -77,7 +77,6 @@ class BaseACL(CommonModelMixin): action = models.CharField(max_length=64, default=ActionChoices.reject, verbose_name=_('Action')) reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers")) is_active = models.BooleanField(default=True, verbose_name=_("Active")) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) ActionChoices = ActionChoices objects = ACLManager.from_queryset(BaseACLQuerySet)() diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index 3db5a738a..b02bc09be 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty, get_logger from orgs.mixins.models import JMSOrgBaseModel - from .base import UserAssetAccountBaseACL logger = get_logger(__file__) @@ -26,7 +25,6 @@ class CommandGroup(JMSOrgBaseModel): ) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case')) - comment = models.TextField(blank=True, verbose_name=_("Comment")) TypeChoices = TypeChoices diff --git a/apps/applications/migrations/0010_appaccount_historicalappaccount.py b/apps/applications/migrations/0010_appaccount_historicalappaccount.py index cd0bf88d1..515754f32 100644 --- a/apps/applications/migrations/0010_appaccount_historicalappaccount.py +++ b/apps/applications/migrations/0010_appaccount_historicalappaccount.py @@ -1,17 +1,17 @@ # Generated by Django 3.1.12 on 2021-08-26 09:07 -import assets.models.base -import common.db.fields -from django.conf import settings +import uuid + import django.core.validators -from django.db import migrations, models import django.db.models.deletion import simple_history.models -import uuid +from django.conf import settings +from django.db import migrations, models + +import common.db.fields class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('assets', '0076_delete_assetuser'), @@ -22,14 +22,19 @@ class Migration(migrations.Migration): migrations.CreateModel( name='HistoricalAccount', fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), - ('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), - ('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[ + django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], + verbose_name='Username')), + ('password', + common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('private_key', + common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), @@ -37,10 +42,17 @@ class Migration(migrations.Migration): ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField()), ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('app', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to='applications.application', verbose_name='Database')), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), + ('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to='assets.systemuser', verbose_name='System user')), ], options={ 'verbose_name': 'historical Account', @@ -52,20 +64,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Account', fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), - ('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), - ('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[ + django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], + verbose_name='Username')), + ('password', + common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('private_key', + common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), ('version', models.IntegerField(default=1, verbose_name='Version')), - ('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')), - ('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')), + ('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, + to='applications.application', verbose_name='Database')), + ('systemuser', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', + verbose_name='System user')), ], options={ 'verbose_name': 'Account', diff --git a/apps/applications/migrations/0027_auto_20221220_1956.py b/apps/applications/migrations/0027_auto_20221220_1956.py new file mode 100644 index 000000000..a72162974 --- /dev/null +++ b/apps/applications/migrations/0027_auto_20221220_1956.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0026_auto_20220817_1716'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='application', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='application', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/applications/models.py b/apps/applications/models.py index f1b1bbc8a..60efc8aab 100644 --- a/apps/applications/models.py +++ b/apps/applications/models.py @@ -1,12 +1,11 @@ - from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.db.models import JMSBaseModel from orgs.mixins.models import OrgModelMixin -from common.mixins import CommonModelMixin -class Application(CommonModelMixin, OrgModelMixin): +class Application(JMSBaseModel, OrgModelMixin): name = models.CharField(max_length=128, verbose_name=_('Name')) category = models.CharField( max_length=16, verbose_name=_('Category') @@ -15,9 +14,6 @@ class Application(CommonModelMixin, OrgModelMixin): max_length=16, verbose_name=_('Type') ) attrs = models.JSONField(default=dict, verbose_name=_('Attrs')) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) class Meta: verbose_name = _('Application') diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 36f734030..67a935f84 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,11 +1,11 @@ -from .mixin import * -from .category import * -from .platform import * -from .asset import * -from .label import * from .account import * -from .node import * -from .domain import * +from .asset import * from .automations import * -from .gathered_user import * +from .category import * +from .domain import * from .favorite_asset import * +from .label import * +from .mixin import * +from .node import * +from .platform import * +from .tree import * diff --git a/apps/assets/api/account/account.py b/apps/assets/api/account/account.py index 714d3d5a2..8dad13b3c 100644 --- a/apps/assets/api/account/account.py +++ b/apps/assets/api/account/account.py @@ -1,19 +1,22 @@ +from django.shortcuts import get_object_or_404 from rest_framework.decorators import action -from rest_framework.response import Response from rest_framework.generics import CreateAPIView, ListAPIView +from rest_framework.response import Response -from orgs.mixins.api import OrgBulkModelViewSet -from rbac.permissions import RBACPermission - +from assets import serializers +from assets.filters import AccountFilterSet +from assets.models import Account, Asset +from assets.tasks import verify_accounts_connectivity +from authentication.const import ConfirmType from common.mixins import RecordViewLogMixin from common.permissions import UserConfirmation -from authentication.const import ConfirmType -from assets.models import Account -from assets.filters import AccountFilterSet -from assets.tasks import verify_accounts_connectivity -from assets import serializers +from orgs.mixins.api import OrgBulkModelViewSet -__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI', 'AccountHistoriesSecretAPI'] +__all__ = [ + 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI', 'AccountHistoriesSecretAPI' +] + +from rbac.permissions import RBACPermission class AccountViewSet(OrgBulkModelViewSet): @@ -30,10 +33,18 @@ class AccountViewSet(OrgBulkModelViewSet): 'su_from_accounts': 'assets.view_account', } - @action(methods=['get'], detail=True, url_path='su-from-accounts') + @action(methods=['get'], detail=False, url_path='su-from-accounts') def su_from_accounts(self, request, *args, **kwargs): - account = super().get_object() - accounts = account.get_su_from_accounts() + account_id = request.query_params.get('account') + asset_id = request.query_params.get('asset') + if account_id: + account = get_object_or_404(Account, pk=account_id) + accounts = account.get_su_from_accounts() + elif asset_id: + asset = get_object_or_404(Asset, pk=asset_id) + accounts = asset.accounts.all() + else: + accounts = [] serializer = serializers.AccountSerializer(accounts, many=True) return Response(data=serializer.data) @@ -54,8 +65,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): 'default': serializers.AccountSecretSerializer, } http_method_names = ['get', 'options'] - # Todo: 记得打开 - # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] rbac_perms = { 'list': 'assets.view_accountsecret', 'retrieve': 'assets.view_accountsecret', @@ -66,8 +76,7 @@ class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView): model = Account.history.model serializer_class = serializers.AccountHistorySerializer http_method_names = ['get', 'options'] - # Todo: 记得打开 - # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] rbac_perms = { 'list': 'assets.view_accountsecret', } @@ -102,4 +111,5 @@ class AccountTaskCreateAPI(CreateAPIView): def get_exception_handler(self): def handler(e, context): return Response({"error": str(e)}, status=400) + return handler diff --git a/apps/assets/api/account/template.py b/apps/assets/api/account/template.py index dd9ee1d00..aa3be6b4f 100644 --- a/apps/assets/api/account/template.py +++ b/apps/assets/api/account/template.py @@ -1,9 +1,6 @@ from assets import serializers from assets.models import AccountTemplate -from rbac.permissions import RBACPermission -from authentication.const import ConfirmType from common.mixins import RecordViewLogMixin -from common.permissions import UserConfirmation from orgs.mixins.api import OrgBulkModelViewSet diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index df139a8c9..9ba11a872 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -30,18 +30,23 @@ __all__ = [ class AssetFilterSet(BaseFilterSet): type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact") - category = django_filters.CharFilter( - field_name="platform__category", lookup_expr="exact" - ) - hostname = django_filters.CharFilter(field_name="name", lookup_expr="exact") + category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact") + platform = django_filters.CharFilter(method='filter_platform') class Meta: model = Asset fields = [ "id", "name", "address", "is_active", - "type", "category", "hostname" + "type", "category", "platform" ] + @staticmethod + def filter_platform(queryset, name, value): + if value.isdigit(): + return queryset.filter(platform_id=value) + else: + return queryset.filter(platform__name=value) + class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): """ @@ -55,8 +60,9 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ordering = ("name",) serializer_classes = ( ("default", serializers.AssetSerializer), - ("suggestion", serializers.MiniAssetSerializer), + ("retrieve", serializers.AssetDetailSerializer), ("platform", serializers.PlatformSerializer), + ("suggestion", serializers.MiniAssetSerializer), ("gateways", serializers.GatewaySerializer), ) rbac_perms = ( diff --git a/apps/assets/api/asset/cloud.py b/apps/assets/api/asset/cloud.py index 64ab6b738..96211ab6e 100644 --- a/apps/assets/api/asset/cloud.py +++ b/apps/assets/api/asset/cloud.py @@ -1,4 +1,4 @@ -from assets.models import Cloud +from assets.models import Cloud, Asset from assets.serializers import CloudSerializer from .asset import AssetViewSet @@ -8,6 +8,7 @@ __all__ = ['CloudViewSet'] class CloudViewSet(AssetViewSet): model = Cloud + perm_model = Asset def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() diff --git a/apps/assets/api/asset/database.py b/apps/assets/api/asset/database.py index d4f135cbf..136f82f54 100644 --- a/apps/assets/api/asset/database.py +++ b/apps/assets/api/asset/database.py @@ -1,4 +1,4 @@ -from assets.models import Database +from assets.models import Database, Asset from assets.serializers import DatabaseSerializer from .asset import AssetViewSet @@ -8,6 +8,7 @@ __all__ = ['DatabaseViewSet'] class DatabaseViewSet(AssetViewSet): model = Database + perm_model = Asset def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() diff --git a/apps/assets/api/asset/device.py b/apps/assets/api/asset/device.py index f6a457fe4..a2031ef1a 100644 --- a/apps/assets/api/asset/device.py +++ b/apps/assets/api/asset/device.py @@ -1,6 +1,5 @@ - from assets.serializers import DeviceSerializer -from assets.models import Device +from assets.models import Device, Asset from .asset import AssetViewSet __all__ = ['DeviceViewSet'] @@ -8,6 +7,7 @@ __all__ = ['DeviceViewSet'] class DeviceViewSet(AssetViewSet): model = Device + perm_model = Asset def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py index fbc2e997c..d2ddc954d 100644 --- a/apps/assets/api/asset/host.py +++ b/apps/assets/api/asset/host.py @@ -1,4 +1,4 @@ -from assets.models import Host +from assets.models import Host, Asset from assets.serializers import HostSerializer from .asset import AssetViewSet @@ -7,6 +7,7 @@ __all__ = ['HostViewSet'] class HostViewSet(AssetViewSet): model = Host + perm_model = Asset def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() diff --git a/apps/assets/api/asset/web.py b/apps/assets/api/asset/web.py index 92aaeff9b..363fd5f49 100644 --- a/apps/assets/api/asset/web.py +++ b/apps/assets/api/asset/web.py @@ -1,4 +1,4 @@ -from assets.models import Web +from assets.models import Web, Asset from assets.serializers import WebSerializer from .asset import AssetViewSet @@ -8,6 +8,7 @@ __all__ = ['WebViewSet'] class WebViewSet(AssetViewSet): model = Web + perm_model = Asset def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() diff --git a/apps/assets/api/automations/base.py b/apps/assets/api/automations/base.py index 1b480cbdc..23c2ef129 100644 --- a/apps/assets/api/automations/base.py +++ b/apps/assets/api/automations/base.py @@ -1,17 +1,18 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from rest_framework.response import Response from rest_framework import status, mixins, viewsets +from rest_framework.response import Response -from orgs.mixins import generics from assets import serializers -from assets.tasks import execute_automation from assets.models import BaseAutomation, AutomationExecution +from assets.tasks import execute_automation from common.const.choices import Trigger +from orgs.mixins import generics __all__ = [ 'AutomationAssetsListApi', 'AutomationRemoveAssetApi', - 'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet' + 'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', + 'AutomationExecutionViewSet', ] diff --git a/apps/assets/api/automations/change_secret.py b/apps/assets/api/automations/change_secret.py index 3cdea7e65..550a3aad0 100644 --- a/apps/assets/api/automations/change_secret.py +++ b/apps/assets/api/automations/change_secret.py @@ -3,14 +3,15 @@ from rest_framework import mixins +from assets import serializers +from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution from common.utils import get_object_or_none from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet - -from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution -from assets import serializers +from .base import AutomationExecutionViewSet __all__ = [ - 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet' + 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', + 'ChangSecretExecutionViewSet' ] @@ -38,3 +39,11 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): queryset = queryset.filter(execution=execution) queryset = queryset.order_by('-date_started') return queryset + + +class ChangSecretExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "assets.view_changesecretexecution"), + ("retrieve", "assets.view_changesecretexecution"), + ("create", "assets.add_changesecretexecution"), + ) diff --git a/apps/assets/api/automations/gather_accounts.py b/apps/assets/api/automations/gather_accounts.py index e7a265f96..ba2d13df0 100644 --- a/apps/assets/api/automations/gather_accounts.py +++ b/apps/assets/api/automations/gather_accounts.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # -from orgs.mixins.api import OrgBulkModelViewSet - -from assets.models import GatherAccountsAutomation from assets import serializers +from assets.models import GatherAccountsAutomation +from orgs.mixins.api import OrgBulkModelViewSet +from .base import AutomationExecutionViewSet __all__ = [ - 'GatherAccountsAutomationViewSet', + 'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet' ] @@ -16,3 +16,11 @@ class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): search_fields = filter_fields ordering_fields = ('name',) serializer_class = serializers.GatherAccountAutomationSerializer + + +class GatherAccountsExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "assets.view_gatheraccountsexecution"), + ("retrieve", "assets.view_gatheraccountsexecution"), + ("create", "assets.add_gatheraccountsexecution"), + ) diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py deleted file mode 100644 index 8fcf59456..000000000 --- a/apps/assets/api/gathered_user.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from orgs.mixins.api import OrgModelViewSet -from assets.models import GatheredUser - -from ..serializers import GatheredUserSerializer -from ..filters import AssetRelatedByNodeFilterBackend - - -__all__ = ['GatheredUserViewSet'] - - -class GatheredUserViewSet(OrgModelViewSet): - model = GatheredUser - serializer_class = GatheredUserSerializer - extra_filter_backends = [AssetRelatedByNodeFilterBackend] - - filterset_fields = ['asset', 'username', 'present', 'asset__address', 'asset__name', 'asset_id'] - search_fields = ['username', 'asset__address', 'asset__name'] diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 2abe967b0..6cc198169 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -61,6 +61,7 @@ class SerializeToTreeNodeMixin: 'meta': { 'type': 'asset', 'data': { + 'platform_type': asset.platform.type, 'org_name': asset.org_name, 'sftp': asset.platform_id in sftp_enabled_platform, }, diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 85935dec2..b45b2170c 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,43 +1,37 @@ # ~*~ coding: utf-8 ~*~ -from functools import partial from collections import namedtuple, defaultdict -from django.core.exceptions import PermissionDenied +from functools import partial -from rest_framework import status -from rest_framework.generics import get_object_or_404 -from rest_framework.serializers import ValidationError -from rest_framework.response import Response -from rest_framework.decorators import action -from django.utils.translation import ugettext_lazy as _ from django.db.models.signals import m2m_changed +from django.utils.translation import ugettext_lazy as _ +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from rest_framework.serializers import ValidationError -from common.const.http import POST -from common.exceptions import SomeoneIsDoingThis -from common.const.signals import PRE_REMOVE, POST_REMOVE -from common.mixins.api import SuggestionMixin from assets.models import Asset +from common.const.http import POST +from common.const.signals import PRE_REMOVE, POST_REMOVE +from common.exceptions import SomeoneIsDoingThis +from common.mixins.api import SuggestionMixin from common.utils import get_logger -from common.tree import TreeNodeSerializer -from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics +from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org +from .. import serializers from ..models import Node from ..tasks import ( update_node_assets_hardware_info_manual, test_node_assets_connectivity_manual, check_node_assets_amount_task ) -from .. import serializers -from ..const import AllTypes -from .mixin import SerializeToTreeNodeMixin -from assets.locks import NodeAddChildrenLock logger = get_logger(__file__) __all__ = [ - 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', - 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi', - 'NodeAddChildrenApi', 'NodeListAsTreeApi', 'NodeChildrenAsTreeApi', - 'NodeTaskCreateApi', 'CategoryTreeApi', + 'NodeViewSet', 'NodeAssetsApi', 'NodeAddAssetsApi', + 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi', + 'NodeAddChildrenApi', 'NodeTaskCreateApi', ] @@ -74,153 +68,6 @@ class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet): return super().destroy(request, *args, **kwargs) -class NodeListAsTreeApi(generics.ListAPIView): - """ - 获取节点列表树 - [ - { - "id": "", - "name": "", - "pId": "", - "meta": "" - } - ] - """ - model = Node - serializer_class = TreeNodeSerializer - - @staticmethod - def to_tree_queryset(queryset): - queryset = [node.as_tree_node() for node in queryset] - return queryset - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.to_tree_queryset(queryset) - return queryset - - -class NodeChildrenApi(generics.ListCreateAPIView): - serializer_class = serializers.NodeSerializer - search_fields = ('value',) - - instance = None - is_initial = False - - def initial(self, request, *args, **kwargs): - self.instance = self.get_object() - return super().initial(request, *args, **kwargs) - - def perform_create(self, serializer): - with NodeAddChildrenLock(self.instance): - data = serializer.validated_data - _id = data.get("id") - value = data.get("value") - if not value: - value = self.instance.get_next_child_preset_name() - node = self.instance.create_child(value=value, _id=_id) - # 避免查询 full value - node._full_value = node.value - serializer.instance = node - - def get_object(self): - pk = self.kwargs.get('pk') or self.request.query_params.get('id') - key = self.request.query_params.get("key") - - if not pk and not key: - self.is_initial = True - if current_org.is_root(): - node = None - else: - node = Node.org_root() - return node - if pk: - node = get_object_or_404(Node, pk=pk) - else: - node = get_object_or_404(Node, key=key) - return node - - def get_org_root_queryset(self, query_all): - if query_all: - return Node.objects.all() - else: - return Node.org_root_nodes() - - def get_queryset(self): - query_all = self.request.query_params.get("all", "0") == "all" - - if self.is_initial and current_org.is_root(): - return self.get_org_root_queryset(query_all) - - if self.is_initial: - with_self = True - else: - with_self = False - - if not self.instance: - return Node.objects.none() - - if query_all: - queryset = self.instance.get_all_children(with_self=with_self) - else: - queryset = self.instance.get_children(with_self=with_self) - return queryset - - -class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): - """ - 节点子节点作为树返回, - [ - { - "id": "", - "name": "", - "pId": "", - "meta": "" - } - ] - - """ - model = Node - - def filter_queryset(self, queryset): - if not self.request.GET.get('search'): - return queryset - queryset = super().filter_queryset(queryset) - queryset = self.model.get_ancestor_queryset(queryset) - return queryset - - def list(self, request, *args, **kwargs): - nodes = self.filter_queryset(self.get_queryset()).order_by('value') - nodes = self.serialize_nodes(nodes, with_asset_amount=True) - assets = self.get_assets() - data = [*nodes, *assets] - return Response(data=data) - - def get_assets(self): - include_assets = self.request.query_params.get('assets', '0') == '1' - if not self.instance or not include_assets: - return [] - assets = self.instance.get_assets().only( - "id", "name", "address", "platform_id", - "org_id", "is_active", - ).prefetch_related('platform') - return self.serialize_assets(assets, self.instance.key) - - -class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): - serializer_class = TreeNodeSerializer - - def check_permissions(self, request): - if not request.user.has_perm('assets.view_asset'): - raise PermissionDenied - return True - - def list(self, request, *args, **kwargs): - nodes = AllTypes.to_tree_nodes() - serializer = self.get_serializer(nodes, many=True) - return Response(data=serializer.data) - - class NodeAssetsApi(generics.ListAPIView): serializer_class = serializers.AssetSerializer diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py index 411bf8b86..dbfcc2e4c 100644 --- a/apps/assets/api/platform.py +++ b/apps/assets/api/platform.py @@ -1,10 +1,10 @@ - +from jumpserver.utils import has_valid_xpack_license from common.drf.api import JMSModelViewSet from common.drf.serializers import GroupedChoiceSerializer from assets.models import Platform +from assets.const import AllTypes from assets.serializers import PlatformSerializer - __all__ = ['AssetPlatformViewSet'] @@ -22,6 +22,11 @@ class AssetPlatformViewSet(JMSModelViewSet): 'ops_methods': 'assets.view_platform' } + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(type__in=AllTypes.get_types()) + return queryset + def get_object(self): pk = self.kwargs.get('pk', '') if pk.isnumeric(): diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py new file mode 100644 index 000000000..2d07d88b0 --- /dev/null +++ b/apps/assets/api/tree.py @@ -0,0 +1,153 @@ +# ~*~ coding: utf-8 ~*~ + +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response + +from assets.locks import NodeAddChildrenLock +from common.tree import TreeNodeSerializer +from common.utils import get_logger +from orgs.mixins import generics +from orgs.utils import current_org +from .mixin import SerializeToTreeNodeMixin +from .. import serializers +from ..const import AllTypes +from ..models import Node, Platform, Asset + +logger = get_logger(__file__) +__all__ = [ + 'NodeChildrenApi', + 'NodeChildrenAsTreeApi', + 'CategoryTreeApi', +] + + +class NodeChildrenApi(generics.ListCreateAPIView): + """ + 节点的增删改查 + """ + serializer_class = serializers.NodeSerializer + search_fields = ('value',) + + instance = None + is_initial = False + + def initial(self, request, *args, **kwargs): + self.instance = self.get_object() + return super().initial(request, *args, **kwargs) + + def perform_create(self, serializer): + with NodeAddChildrenLock(self.instance): + data = serializer.validated_data + _id = data.get("id") + value = data.get("value") + if not value: + value = self.instance.get_next_child_preset_name() + node = self.instance.create_child(value=value, _id=_id) + # 避免查询 full value + node._full_value = node.value + serializer.instance = node + + def get_object(self): + pk = self.kwargs.get('pk') or self.request.query_params.get('id') + key = self.request.query_params.get("key") + + if not pk and not key: + self.is_initial = True + if current_org.is_root(): + node = None + else: + node = Node.org_root() + return node + if pk: + node = get_object_or_404(Node, pk=pk) + else: + node = get_object_or_404(Node, key=key) + return node + + def get_org_root_queryset(self, query_all): + if query_all: + return Node.objects.all() + else: + return Node.org_root_nodes() + + def get_queryset(self): + query_all = self.request.query_params.get("all", "0") == "all" + + if self.is_initial and current_org.is_root(): + return self.get_org_root_queryset(query_all) + + if self.is_initial: + with_self = True + else: + with_self = False + + if not self.instance: + return Node.objects.none() + + if query_all: + queryset = self.instance.get_all_children(with_self=with_self) + else: + queryset = self.instance.get_children(with_self=with_self) + return queryset + + +class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): + """ + 节点子节点作为树返回, + [ + { + "id": "", + "name": "", + "pId": "", + "meta": "" + } + ] + + """ + model = Node + + def filter_queryset(self, queryset): + if not self.request.GET.get('search'): + return queryset + queryset = super().filter_queryset(queryset) + queryset = self.model.get_ancestor_queryset(queryset) + return queryset + + def list(self, request, *args, **kwargs): + nodes = self.filter_queryset(self.get_queryset()).order_by('value') + nodes = self.serialize_nodes(nodes, with_asset_amount=True) + assets = self.get_assets_as_node() + data = [*nodes, *assets] + return Response(data=data) + + def get_assets_as_node(self): + include_assets = self.request.query_params.get('assets', '0') == '1' + if not self.instance or not include_assets: + return [] + assets = self.instance.get_assets_for_tree() + return self.serialize_assets(assets, self.instance.key) + + +class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): + serializer_class = TreeNodeSerializer + rbac_perms = { + 'GET': 'assets.view_asset', + 'list': 'assets.view_asset', + } + + def get_assets(self): + key = self.request.query_params.get('key') + platform = Platform.objects.filter(id=key).first() + if not platform: + return [] + assets = Asset.objects.filter(platform=platform).prefetch_related('platform') + return self.serialize_assets(assets, key) + + def list(self, request, *args, **kwargs): + include_asset = self.request.query_params.get('assets', '0') == '1' + + if include_asset and self.request.query_params.get('key'): + nodes = self.get_assets() + else: + nodes = AllTypes.to_tree_nodes(include_asset) + return Response(data=nodes) diff --git a/apps/assets/automations/backup_account/handlers.py b/apps/assets/automations/backup_account/handlers.py index 1575ce8fd..5c4ffce76 100644 --- a/apps/assets/automations/backup_account/handlers.py +++ b/apps/assets/automations/backup_account/handlers.py @@ -49,23 +49,6 @@ class BaseAccountHandler: header_fields[field] = str(v.label) return header_fields - @staticmethod - def load_auth(tp, value, system_user): - if value: - return value - if system_user: - return getattr(system_user, tp, '') - return '' - - @classmethod - def replace_auth(cls, account, system_user_dict): - system_user = system_user_dict.get(account.systemuser_id) - account.username = cls.load_auth('username', account.username, system_user) - account.password = cls.load_auth('password', account.password, system_user) - account.private_key = cls.load_auth('private_key', account.private_key, system_user) - account.public_key = cls.load_auth('public_key', account.public_key, system_user) - return account - @classmethod def create_row(cls, data, header_fields): data = cls.unpack_data(data) @@ -94,30 +77,30 @@ class AssetAccountHandler(BaseAccountHandler): return filename @classmethod - def create_data_map(cls, categories: list): + def create_data_map(cls, types: list): data_map = defaultdict(list) # TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作 qs = Account.objects.filter( - asset__platform__type__in=categories - ).annotate(category=F('asset__platform__type')) - print(qs, categories) + asset__platform__type__in=types + ).annotate(type=F('asset__platform__type')) + if not qs.exists(): return data_map - category_dict = {} + type_dict = {} for i in AllTypes.grouped_choices_to_objs(): for j in i['children']: - category_dict[j['value']] = j['display_name'] + type_dict[j['value']] = j['display_name'] header_fields = cls.get_header_fields(AccountSecretSerializer(qs.first())) - account_category_map = defaultdict(list) + account_type_map = defaultdict(list) for account in qs: - account_category_map[account.category].append(account) + account_type_map[account.type].append(account) data_map = {} - for category, accounts in account_category_map.items(): - sheet_name = category_dict.get(category, category) + for tp, accounts in account_type_map.items(): + sheet_name = type_dict.get(tp, tp) data = AccountSecretSerializer(accounts, many=True).data data_map.update(cls.add_rows(data, header_fields, sheet_name)) @@ -140,9 +123,9 @@ class AccountBackupHandler: # Print task start date time_start = time.time() files = [] - categories = self.execution.categories + types = self.execution.types - data_map = AssetAccountHandler.create_data_map(categories) + data_map = AssetAccountHandler.create_data_map(types) if not data_map: return files diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 6d1563258..b2f76b43d 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -1,19 +1,19 @@ import os -import yaml import shutil -from hashlib import md5 -from copy import deepcopy -from socket import gethostname from collections import defaultdict +from copy import deepcopy +from hashlib import md5 +from socket import gethostname +import yaml from django.conf import settings from django.utils import timezone from django.utils.translation import gettext as _ -from common.utils import get_logger -from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj -from assets.const import SecretType from assets.automations.methods import platform_automation_methods +from assets.const import SecretType +from common.utils import get_logger, lazyproperty +from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) @@ -93,7 +93,7 @@ class BasePlaybookManager: def get_assets_group_by_platform(self): return self.automation.all_assets_group_by_platform() - @property + @lazyproperty def runtime_dir(self): ansible_dir = settings.ANSIBLE_DIR dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id) @@ -105,11 +105,6 @@ class BasePlaybookManager: os.makedirs(path, exist_ok=True, mode=0o755) return path - def prepare_playbook_dir(self): - for d in [self.runtime_dir]: - if not os.path.exists(d): - os.makedirs(d, exist_ok=True, mode=0o755) - def host_callback(self, host, automation=None, **kwargs): enabled_attr = '{}_enabled'.format(self.__class__.method_type()) method_attr = '{}_method'.format(self.__class__.method_type()) diff --git a/apps/assets/automations/gather_accounts/manager.py b/apps/assets/automations/gather_accounts/manager.py index 40253be82..da1b44abe 100644 --- a/apps/assets/automations/gather_accounts/manager.py +++ b/apps/assets/automations/gather_accounts/manager.py @@ -1,8 +1,9 @@ +from django.utils.translation import ugettext_lazy as _ + from common.utils import get_logger -from assets.const import AutomationTypes +from assets.const import AutomationTypes, Source from orgs.utils import tmp_to_org from .filter import GatherAccountsFilter -from ...models import GatheredUser from ..base.manager import BasePlaybookManager logger = get_logger(__name__) @@ -26,20 +27,33 @@ class GatherAccountsManager(BasePlaybookManager): result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result) return result + @staticmethod + def bulk_create_accounts(asset, result): + account_objs = [] + account_model = asset.accounts.model + account_usernames = set(asset.accounts.values_list('username', flat=True)) + with tmp_to_org(asset.org_id): + accounts_dict = {} + for username, data in result.items(): + comment = '' + d = {'asset': asset, 'username': username, 'name': username, 'source': Source.COLLECTED} + if data.get('date'): + comment += f"{_('Date last login')}: {data['date']}\n " + if data.get('address'): + comment += f"{_('IP last login')}: {data['address'][:32]}" + d['comment'] = comment + accounts_dict[username] = d + for username, data in accounts_dict.items(): + if username in account_usernames: + continue + account_objs.append(account_model(**data)) + account_model.objects.bulk_create(account_objs) + def on_host_success(self, host, result): info = result.get('debug', {}).get('res', {}).get('info', {}) asset = self.host_asset_mapper.get(host) - org_id = asset.org_id if asset and info: result = self.filter_success_result(host, info) - with tmp_to_org(org_id): - GatheredUser.objects.filter(asset=asset, present=True).update(present=False) - for username, data in result.items(): - defaults = {'asset': asset, 'present': True, 'username': username} - if data.get('date'): - defaults['date_last_login'] = data['date'] - if data.get('address'): - defaults['ip_last_login'] = data['address'][:32] - GatheredUser.objects.update_or_create(defaults=defaults, asset=asset, username=username) + self.bulk_create_accounts(asset, result) else: logger.error("Not found info".format(host)) diff --git a/apps/assets/automations/push_account/database/mongodb/main.yml b/apps/assets/automations/push_account/database/mongodb/main.yml deleted file mode 100644 index d516251db..000000000 --- a/apps/assets/automations/push_account/database/mongodb/main.yml +++ /dev/null @@ -1,16 +0,0 @@ -- hosts: mongodb - gather_facts: no - vars: - ansible_python_interpreter: /usr/local/bin/python - - tasks: - - name: Add user account.username - mongodb_user: - login_user: "{{ jms_account.username }}" - login_password: "{{ jms_account.secret }}" - login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" - login_database: "{{ jms_asset.specific.db_name }}" - db: "{{ jms_asset.specific.db_name }}" - name: "{{ account.username }}" - password: "{{ account.secret }}" diff --git a/apps/assets/automations/push_account/database/mongodb/manifest.yml b/apps/assets/automations/push_account/database/mongodb/manifest.yml deleted file mode 100644 index 9de93f5e7..000000000 --- a/apps/assets/automations/push_account/database/mongodb/manifest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: push_account_mongodb -name: Push account from MongoDB -category: database -type: - - mongodb -method: push_account diff --git a/apps/assets/automations/push_account/database/mysql/main.yml b/apps/assets/automations/push_account/database/mysql/main.yml deleted file mode 100644 index bf10c95af..000000000 --- a/apps/assets/automations/push_account/database/mysql/main.yml +++ /dev/null @@ -1,15 +0,0 @@ -- hosts: mysql - gather_facts: no - vars: - ansible_python_interpreter: /usr/local/bin/python - - tasks: - - name: Add user account.username - community.mysql.mysql_user: - login_user: "{{ jms_account.username }}" - login_password: "{{ jms_account.secret }}" - login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" - name: "{{ account.username }}" - password: "{{ account.secret }}" - host: "%" diff --git a/apps/assets/automations/push_account/database/mysql/manifest.yml b/apps/assets/automations/push_account/database/mysql/manifest.yml deleted file mode 100644 index d954cb1d8..000000000 --- a/apps/assets/automations/push_account/database/mysql/manifest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: push_account_mysql -name: Push account from MySQL -category: database -type: - - mysql -method: push_account diff --git a/apps/assets/automations/push_account/database/oracle/main.yml b/apps/assets/automations/push_account/database/oracle/main.yml deleted file mode 100644 index 5812dfee1..000000000 --- a/apps/assets/automations/push_account/database/oracle/main.yml +++ /dev/null @@ -1,16 +0,0 @@ -- hosts: oracle - gather_facts: no - vars: - ansible_python_interpreter: /usr/local/bin/python - - tasks: - - name: Add user account.username - oracle_user: - login_user: "{{ jms_account.username }}" - login_password: "{{ jms_account.secret }}" - login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" - login_database: "{{ jms_asset.specific.db_name }}" - mode: "{{ jms_account.mode }}" - name: "{{ account.username }}" - password: "{{ account.secret }}" diff --git a/apps/assets/automations/push_account/database/oracle/manifest.yml b/apps/assets/automations/push_account/database/oracle/manifest.yml deleted file mode 100644 index da1faed6f..000000000 --- a/apps/assets/automations/push_account/database/oracle/manifest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: push_account_oracle -name: Push account from Oracle -category: database -type: - - oracle -method: push_account diff --git a/apps/assets/automations/push_account/database/postgresql/main.yml b/apps/assets/automations/push_account/database/postgresql/main.yml deleted file mode 100644 index febb213c4..000000000 --- a/apps/assets/automations/push_account/database/postgresql/main.yml +++ /dev/null @@ -1,16 +0,0 @@ -- hosts: postgresql - gather_facts: no - vars: - ansible_python_interpreter: /usr/local/bin/python - - tasks: - - name: Add user account.username - community.postgresql.postgresql_user: - login_user: "{{ jms_account.username }}" - login_password: "{{ jms_account.secret }}" - login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" - db: "{{ jms_asset.specific.db_name }}" - name: "{{ account.username }}" - password: "{{ account.secret }}" - diff --git a/apps/assets/automations/push_account/database/postgresql/manifest.yml b/apps/assets/automations/push_account/database/postgresql/manifest.yml deleted file mode 100644 index 6488ddd5a..000000000 --- a/apps/assets/automations/push_account/database/postgresql/manifest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: push_account_postgresql -name: Push account for PostgreSQL -category: database -type: - - postgresql -method: push_account diff --git a/apps/assets/automations/push_account/host/posix/main.yml b/apps/assets/automations/push_account/host/posix/main.yml deleted file mode 100644 index e78c57152..000000000 --- a/apps/assets/automations/push_account/host/posix/main.yml +++ /dev/null @@ -1,19 +0,0 @@ -- hosts: demo - gather_facts: no - tasks: - - name: Add user account.username - ansible.builtin.user: - name: "{{ account.username }}" - - - name: Set account.username password - ansible.builtin.user: - name: "{{ account.username }}" - password: "{{ account.secret | password_hash('sha512') }}" - update_password: always - when: secret_type == "password" - - - name: Set account.username SSH key - ansible.builtin.authorized_key: - user: "{{ account.username }}" - key: "{{ account.secret }}" - when: secret_type == "ssh_key" diff --git a/apps/assets/automations/push_account/host/posix/manifest.yml b/apps/assets/automations/push_account/host/posix/manifest.yml deleted file mode 100644 index 9a7cc5c8c..000000000 --- a/apps/assets/automations/push_account/host/posix/manifest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: push_account_posix -name: Push posix account -category: host -type: - - linux - - unix -method: push_account diff --git a/apps/assets/automations/push_account/host/windows/main.yml b/apps/assets/automations/push_account/host/windows/main.yml deleted file mode 100644 index bbe8219d0..000000000 --- a/apps/assets/automations/push_account/host/windows/main.yml +++ /dev/null @@ -1,13 +0,0 @@ -- hosts: windows - gather_facts: yes - tasks: - - name: Add user account.username - ansible.windows.win_user: - vars: - fullname: "{{ account.username }}" - name: "{{ account.username }}" - password: "{{ account.secret }}" - state: present - password_expired: no - update_password: always - password_never_expires: yes diff --git a/apps/assets/automations/push_account/host/windows/manifest.yml b/apps/assets/automations/push_account/host/windows/manifest.yml deleted file mode 100644 index 7e0256f44..000000000 --- a/apps/assets/automations/push_account/host/windows/manifest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: push_account_windows -name: Push account windows -version: 1 -method: push_account -category: host -type: - - windows diff --git a/apps/assets/const/account.py b/apps/assets/const/account.py index ebeb855ed..4c15bb519 100644 --- a/apps/assets/const/account.py +++ b/apps/assets/const/account.py @@ -13,3 +13,14 @@ class SecretType(TextChoices): SSH_KEY = 'ssh_key', _('SSH key') ACCESS_KEY = 'access_key', _('Access key') TOKEN = 'token', _('Token') + + +class AliasAccount(TextChoices): + ALL = '@ALL', _('All') + INPUT = '@INPUT', _('Manual input') + USER = '@USER', _('Dynamic user') + + +class Source(TextChoices): + LOCAL = 'local', _('Local') + COLLECTED = 'collected', _('Collected') diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py index 99acefa7a..72cd5704e 100644 --- a/apps/assets/const/automation.py +++ b/apps/assets/const/automation.py @@ -12,7 +12,7 @@ DEFAULT_PASSWORD_RULES = { class AutomationTypes(TextChoices): ping = 'ping', _('Ping') gather_facts = 'gather_facts', _('Gather facts') - push_account = 'push_account', _('Create account') + push_account = 'push_account', _('Push account') change_secret = 'change_secret', _('Change secret') verify_account = 'verify_account', _('Verify account') gather_accounts = 'gather_accounts', _('Gather accounts') @@ -20,8 +20,9 @@ class AutomationTypes(TextChoices): @classmethod def get_type_model(cls, tp): from assets.models import ( - PingAutomation, GatherFactsAutomation, PushAccountAutomation, - ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation, + PingAutomation, GatherFactsAutomation, + PushAccountAutomation, ChangeSecretAutomation, + VerifyAccountAutomation, GatherAccountsAutomation, ) type_model_dict = { cls.ping: PingAutomation, diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py index 35ad76757..aa5070d5e 100644 --- a/apps/assets/const/base.py +++ b/apps/assets/const/base.py @@ -1,5 +1,6 @@ from django.db.models import TextChoices +from jumpserver.utils import has_valid_xpack_license from .protocol import Protocol @@ -53,3 +54,25 @@ class BaseType(TextChoices): @classmethod def internal_platforms(cls): raise NotImplementedError + + @classmethod + def get_community_types(cls): + raise NotImplementedError + + @classmethod + def get_types(cls): + tps = [tp for tp in cls] + if not has_valid_xpack_license(): + tps = cls.get_community_types() + return tps + + @classmethod + def get_choices(cls): + tps = cls.get_types() + cls_choices = cls.choices + return [ + choice for choice in cls_choices + if choice[0] in tps + ] + + diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index 7bc1864f1..22240cc77 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -25,7 +25,6 @@ class CloudTypes(BaseType): 'gather_facts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, - 'push_account_enabled': False, 'gather_accounts_enabled': False, } } @@ -49,3 +48,9 @@ class CloudTypes(BaseType): cls.PRIVATE: [{'name': 'Vmware-vSphere'}], cls.K8S: [{'name': 'Kubernetes'}], } + + @classmethod + def get_community_types(cls): + return [ + cls.K8S, cls.PUBLIC, cls.PRIVATE + ] diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py index 40b9f8aff..53c06bff0 100644 --- a/apps/assets/const/database.py +++ b/apps/assets/const/database.py @@ -1,4 +1,3 @@ - from .base import BaseType @@ -34,7 +33,6 @@ class DatabaseTypes(BaseType): 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, - 'push_account_enabled': True, } } return constrains @@ -62,3 +60,8 @@ class DatabaseTypes(BaseType): cls.REDIS: [{'name': 'Redis'}], } + @classmethod + def get_community_types(cls): + return [ + cls.MYSQL, cls.MARIADB, cls.MONGODB, cls.REDIS + ] diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index 1e2a5b717..cbd9d7b27 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -40,7 +40,6 @@ class DeviceTypes(BaseType): 'gather_accounts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, - 'push_account_enabled': False, } } @@ -52,3 +51,9 @@ class DeviceTypes(BaseType): cls.ROUTER: [], cls.FIREWALL: [] } + + @classmethod + def get_community_types(cls): + return [ + cls.GENERAL, cls.SWITCH, cls.ROUTER, cls.FIREWALL + ] diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 8be44db6f..91c6b51be 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -34,7 +34,7 @@ class HostTypes(BaseType): def _get_protocol_constrains(cls) -> dict: return { '*': { - 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] + 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] }, cls.WINDOWS: { 'choices': ['rdp', 'ssh', 'vnc'] @@ -54,7 +54,6 @@ class HostTypes(BaseType): 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, - 'push_account_enabled': True, }, cls.WINDOWS: { 'ansible_config': { @@ -76,7 +75,6 @@ class HostTypes(BaseType): {'name': 'macOS'}, {'name': 'BSD'}, {'name': 'AIX', 'automation': { - 'push_account_method': 'push_account_aix', 'change_secret_method': 'push_secret_aix' }} ], @@ -97,7 +95,7 @@ class HostTypes(BaseType): { 'name': 'RemoteAppHost', '_protocols': ['rdp', 'ssh'], - 'protocols_setting': { + 'protocols_setting': { 'ssh': { 'required': True } @@ -106,3 +104,9 @@ class HostTypes(BaseType): ], cls.OTHER_HOST: [] } + + @classmethod + def get_community_types(cls) -> list: + return [ + cls.LINUX, cls.UNIX, cls.WINDOWS, cls.OTHER_HOST + ] diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index d1ce663b8..7875e5ef4 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -1,4 +1,5 @@ from django.db import models + from common.db.models import ChoicesMixin __all__ = ['Protocol'] @@ -102,9 +103,9 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 80, 'secret_types': ['password'], 'setting': { - 'username_selector': 'input[type=text]', - 'password_selector': 'input[type=password]', - 'submit_selector': 'button[type=submit]', + 'username_selector': 'name=username', + 'password_selector': 'name=password', + 'submit_selector': 'id=longin_button', } }, } @@ -112,7 +113,7 @@ class Protocol(ChoicesMixin, models.TextChoices): @classmethod def settings(cls): return { - **cls.device_protocols(), - **cls.database_protocols(), - **cls.cloud_protocols() + **cls.device_protocols(), + **cls.database_protocols(), + **cls.cloud_protocols() } diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index f9cf83b85..1bf61a583 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -1,14 +1,15 @@ +from collections import defaultdict from copy import deepcopy -from common.db.models import ChoicesMixin -from common.tree import TreeNode +from django.utils.translation import gettext as _ +from common.db.models import ChoicesMixin from .category import Category -from .host import HostTypes -from .device import DeviceTypes -from .database import DatabaseTypes -from .web import WebTypes from .cloud import CloudTypes +from .database import DatabaseTypes +from .device import DeviceTypes +from .host import HostTypes +from .web import WebTypes class AllTypes(ChoicesMixin): @@ -54,7 +55,7 @@ class AllTypes(ChoicesMixin): item_name = item.replace('_enabled', '') methods = filter_platform_methods(category, tp, item_name) methods = [{'name': m['name'], 'id': m['id']} for m in methods] - automation_methods[item_name+'_methods'] = methods + automation_methods[item_name + '_methods'] = methods automation.update(automation_methods) constraints['automation'] = automation return constraints @@ -62,14 +63,18 @@ class AllTypes(ChoicesMixin): @classmethod def types(cls, with_constraints=True): types = [] - for category, tps in cls.category_types(): + for category, type_cls in cls.category_types(): + tps = type_cls.get_types() types.extend([cls.serialize_type(category, tp, with_constraints) for tp in tps]) return types @classmethod def categories(cls, with_constraints=True): categories = [] - for category, tps in cls.category_types(): + for category, type_cls in cls.category_types(): + tps = type_cls.get_types() + if not tps: + continue category_data = { 'value': category.value, 'label': category.label, @@ -121,30 +126,82 @@ class AllTypes(ChoicesMixin): (Category.CLOUD, CloudTypes) ) + @classmethod + def get_types(cls): + tps = [] + for i in dict(cls.category_types()).values(): + tps.extend(i.get_types()) + return tps + @staticmethod def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None): - node = TreeNode(**{ - 'id': choice.name, + node = { + 'id': pid + '_' + choice.name, 'name': choice.label, 'title': choice.label, 'pId': pid, 'open': opened, 'isParent': is_parent, - }) + } if meta: - node.meta = meta + node['meta'] = meta return node @classmethod - def to_tree_nodes(cls): - root = TreeNode(id='ROOT', name='类型节点', title='类型节点') + def platform_to_node(cls, p, pid, include_asset): + node = { + 'id': '{}'.format(p.id), + 'name': p.name, + 'title': p.name, + 'pId': pid, + 'isParent': include_asset, + 'meta': { + 'type': 'platform' + } + } + return node + + @classmethod + def to_tree_nodes(cls, include_asset): + from ..models import Asset, Platform + asset_platforms = Asset.objects.all().values_list('platform_id', flat=True) + platform_count = defaultdict(int) + for platform_id in asset_platforms: + platform_count[platform_id] += 1 + + category_type_mapper = defaultdict(int) + platforms = Platform.objects.all() + tp_platforms = defaultdict(list) + + for p in platforms: + category_type_mapper[p.category + '_' + p.type] += platform_count[p.id] + category_type_mapper[p.category] += platform_count[p.id] + tp_platforms[p.category + '_' + p.type].append(p) + + root = dict(id='ROOT', name=_('All types'), title='所有类型', open=True, isParent=True) nodes = [root] - for category, types in cls.category_types(): - category_node = cls.choice_to_node(category, 'ROOT', meta={'type': 'category'}) + for category, type_cls in cls.category_types(): + # Category 格式化 + meta = {'type': 'category', 'category': category.value} + category_node = cls.choice_to_node(category, 'ROOT', meta=meta) + category_count = category_type_mapper.get(category, 0) + category_node['name'] += f'({category_count})' nodes.append(category_node) + + # Type 格式化 + types = type_cls.get_types() for tp in types: - tp_node = cls.choice_to_node(tp, category_node.id, meta={'type': 'type'}) + meta = {'type': 'type', 'category': category.value, '_type': tp.value} + tp_node = cls.choice_to_node(tp, category_node['id'], opened=False, meta=meta) + tp_count = category_type_mapper.get(category + '_' + tp, 0) + tp_node['name'] += f'({tp_count})' nodes.append(tp_node) + + # Platform 格式化 + for p in tp_platforms.get(category + '_' + tp, []): + platform_node = cls.platform_to_node(p, tp_node['id'], include_asset) + platform_node['name'] += f'({platform_count.get(p.id, 0)})' + nodes.append(platform_node) return nodes @classmethod @@ -253,8 +310,3 @@ class AllTypes(ChoicesMixin): print("\t- Update platform: {}".format(platform.name)) platform_data = cls.get_type_default_platform(platform.category, platform.type) cls.create_or_update_by_platform_data(platform.name, platform_data) - - - - - diff --git a/apps/assets/const/web.py b/apps/assets/const/web.py index 20c35b3a1..88ff7f8f9 100644 --- a/apps/assets/const/web.py +++ b/apps/assets/const/web.py @@ -44,3 +44,9 @@ class WebTypes(BaseType): {'name': 'Website'}, ], } + + @classmethod + def get_community_types(cls): + return [ + cls.WEBSITE, + ] diff --git a/apps/assets/filters.py b/apps/assets/filters.py index f1b869805..c60d492e3 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -126,17 +126,6 @@ class LabelFilterBackend(filters.BaseFilterBackend): return queryset -class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend): - def filter_node_related_all(self, queryset, node): - return queryset.filter( - Q(asset__nodes__key__istartswith=f'{node.key}:') | - Q(asset__nodes__key=node.key) - ).distinct() - - def filter_node_related_direct(self, queryset, node): - return queryset.filter(asset__nodes__key=node.key).distinct() - - class IpInFilterBackend(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): ips = request.query_params.get('ips') diff --git a/apps/assets/migrations/0095_auto_20220407_1726.py b/apps/assets/migrations/0095_auto_20220407_1726.py index c11517a35..9d9baf42c 100644 --- a/apps/assets/migrations/0095_auto_20220407_1726.py +++ b/apps/assets/migrations/0095_auto_20220407_1726.py @@ -12,7 +12,6 @@ def migrate_platform_type_to_lower(apps, *args): class Migration(migrations.Migration): - dependencies = [ ('assets', '0094_auto_20220402_1736'), ] @@ -51,7 +50,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='platform', name='su_method', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='SU method'), + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Su method'), ), migrations.RunPython(migrate_platform_type_to_lower) ] diff --git a/apps/assets/migrations/0096_auto_20220426_1550.py b/apps/assets/migrations/0096_auto_20220426_1550.py index 8d7e6be27..30c4b76be 100644 --- a/apps/assets/migrations/0096_auto_20220426_1550.py +++ b/apps/assets/migrations/0096_auto_20220426_1550.py @@ -1,11 +1,9 @@ # Generated by Django 3.1.14 on 2022-04-26 07:54 from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ ('assets', '0095_auto_20220407_1726'), ] @@ -18,7 +16,8 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=32, verbose_name='Name')), ('port', models.IntegerField(verbose_name='Port')), ('setting', models.JSONField(default=dict, verbose_name='Setting')), - ('platform', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.platform'),), + ('platform', + models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.platform'),), ('default', models.BooleanField(default=True, verbose_name='Default')), ('required', models.BooleanField(default=False, verbose_name='Required')), ], @@ -32,20 +31,24 @@ class Migration(migrations.Migration): ('ping_enabled', models.BooleanField(default=False, verbose_name='Ping enabled')), ('ping_method', models.CharField(blank=True, max_length=32, null=True, verbose_name='Ping method')), ('gather_facts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')), - ('gather_facts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), - ('push_account_enabled', models.BooleanField(default=False, verbose_name='Create account enabled')), - ('push_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Create account method')), + ('gather_facts_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), ('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')), - ('change_secret_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')), + ('change_secret_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')), ('verify_account_enabled', models.BooleanField(default=False, verbose_name='Verify account enabled')), - ('verify_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')), + ('verify_account_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')), ('gather_accounts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')), - ('gather_accounts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), + ('gather_accounts_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), ], ), migrations.AddField( model_name='platform', name='automation', - field=models.OneToOneField(blank=True, null=True, on_delete=models.deletion.CASCADE, related_name='platform', to='assets.platformautomation', verbose_name='Automation'), + field=models.OneToOneField(blank=True, null=True, on_delete=models.deletion.CASCADE, + related_name='platform', to='assets.platformautomation', + verbose_name='Automation'), ), ] diff --git a/apps/assets/migrations/0099_auto_20220711_1409.py b/apps/assets/migrations/0099_auto_20220711_1409.py index 01ad1cf1f..58f917ac4 100644 --- a/apps/assets/migrations/0099_auto_20220711_1409.py +++ b/apps/assets/migrations/0099_auto_20220711_1409.py @@ -1,15 +1,16 @@ # Generated by Django 3.2.12 on 2022-07-11 08:59 -import common.db.fields -from django.conf import settings -from django.db import migrations, models +import uuid + import django.db.models.deletion import simple_history.models -import uuid +from django.conf import settings +from django.db import migrations, models + +import common.db.fields class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('assets', '0098_auto_20220430_2126'), @@ -20,14 +21,19 @@ class Migration(migrations.Migration): name='HistoricalAccount', fields=[ ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), - ('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('version', models.IntegerField(default=0, verbose_name='Version'),), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField(db_index=True)), ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), ], options={ 'verbose_name': 'historical Account', @@ -40,43 +46,55 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Account', fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, verbose_name='Name')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], + default='unknown', max_length=16, verbose_name='Connectivity')), ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('version', models.IntegerField(default=0, verbose_name='Version')), - ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='assets.asset', verbose_name='Asset')), + ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', + to='assets.asset', verbose_name='Asset')), ], options={ 'verbose_name': 'Account', - 'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')], + 'permissions': [('view_accountsecret', 'Can view asset account secret'), + ('change_accountsecret', 'Can change asset account secret'), + ('view_historyaccount', 'Can view asset history account'), + ('view_historyaccountsecret', 'Can view asset history account secret')], 'unique_together': {('name', 'asset'), ('username', 'asset', 'secret_type')}, }, ), migrations.AddField( model_name='account', name='su_from', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='assets.account', verbose_name='Su from'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', + to='assets.account', verbose_name='Su from'), ), migrations.CreateModel( name='AccountTemplate', fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, verbose_name='Name')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), diff --git a/apps/assets/migrations/0107_auto_20221019_1115.py b/apps/assets/migrations/0107_auto_20221019_1115.py index 2dea81e71..7951f56f1 100644 --- a/apps/assets/migrations/0107_auto_20221019_1115.py +++ b/apps/assets/migrations/0107_auto_20221019_1115.py @@ -75,33 +75,11 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='platformautomation', - name='push_account_enabled', - field=models.BooleanField(default=False, verbose_name='Push account enabled'), - ), - migrations.AlterField( - model_name='platformautomation', - name='push_account_method', - field=models.TextField(blank=True, max_length=32, null=True, verbose_name='Push account method'), - ), migrations.AlterField( model_name='platformprotocol', name='default', field=models.BooleanField(default=False, verbose_name='Default'), ), - migrations.CreateModel( - name='DiscoveryAccountAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Discovery account automation', - }, - bases=('assets.baseautomation',), - ), migrations.CreateModel( name='GatherFactsAutomation', fields=[ diff --git a/apps/assets/migrations/0114_remove_redundant_macos.py b/apps/assets/migrations/0114_remove_redundant_macos.py new file mode 100644 index 000000000..d24a3e74a --- /dev/null +++ b/apps/assets/migrations/0114_remove_redundant_macos.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.14 on 2022-12-15 07:08 + +from django.db import migrations + + +def migrate_del_macos(apps, schema_editor): + db_alias = schema_editor.connection.alias + asset_model = apps.get_model('assets', 'Asset') + platform_model = apps.get_model('assets', 'Platform') + old_macos = platform_model.objects.using(db_alias).filter( + name='MacOS', type='macos' + ).first() + new_macos = platform_model.objects.using(db_alias).filter( + name='macOS', type='unix' + ).first() + + if not old_macos or not new_macos: + return + + asset_model.objects.using(db_alias).filter( + platform=old_macos + ).update(platform=new_macos) + + platform_model.objects.using(db_alias).filter(id=old_macos.id).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0113_auto_20221122_2015'), + ] + + operations = [ + migrations.RunPython(migrate_del_macos), + ] diff --git a/apps/assets/migrations/0115_auto_20221220_1956.py b/apps/assets/migrations/0115_auto_20221220_1956.py new file mode 100644 index 000000000..976a8c53b --- /dev/null +++ b/apps/assets/migrations/0115_auto_20221220_1956.py @@ -0,0 +1,183 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0114_remove_redundant_macos'), + ] + + operations = [ + migrations.AddField( + model_name='accountbackupplan', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='baseautomation', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='changesecretrecord', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='domain', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='domain', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='domain', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='favoriteasset', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='favoriteasset', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='gathereduser', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='gathereduser', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='gathereduser', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='node', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='node', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='node', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AddField( + model_name='node', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='node', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='account', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='account', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='accountbackupplan', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='accountbackupplan', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='accounttemplate', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='accounttemplate', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='asset', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='asset', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='baseautomation', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='baseautomation', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='changesecretrecord', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='changesecretrecord', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='domain', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='favoriteasset', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='gathereduser', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='label', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='label', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='label', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/assets/migrations/0116_alter_automationexecution_options.py b/apps/assets/migrations/0116_alter_automationexecution_options.py new file mode 100644 index 000000000..75ae53efd --- /dev/null +++ b/apps/assets/migrations/0116_alter_automationexecution_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2022-12-22 11:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0115_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterModelOptions( + name='automationexecution', + options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexection', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution')], 'verbose_name': 'Automation task execution'}, + ), + ] diff --git a/apps/assets/migrations/0117_alter_gateway_options.py b/apps/assets/migrations/0117_alter_gateway_options.py new file mode 100644 index 000000000..826535b3f --- /dev/null +++ b/apps/assets/migrations/0117_alter_gateway_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2022-12-23 07:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0116_alter_automationexecution_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='gateway', + options={'verbose_name': 'Gateway'}, + ), + ] diff --git a/apps/assets/migrations/0118_auto_20221227_1504.py b/apps/assets/migrations/0118_auto_20221227_1504.py new file mode 100644 index 000000000..47452733a --- /dev/null +++ b/apps/assets/migrations/0118_auto_20221227_1504.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.14 on 2022-12-27 07:04 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0117_alter_gateway_options'), + ] + + operations = [ + migrations.AddField( + model_name='pushaccountautomation', + name='password_rules', + field=models.JSONField(default=dict, verbose_name='Password rules'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='secret', + field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='secret_strategy', + field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='secret_type', + field=models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='ssh_key_change_strategy', + field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='username', + field=models.CharField(default='', max_length=128, verbose_name='Username'), + preserve_default=False, + ), + migrations.AlterField( + model_name='baseautomation', + name='type', + field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Push account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/assets/migrations/0119_auto_20221227_1740.py b/apps/assets/migrations/0119_auto_20221227_1740.py new file mode 100644 index 000000000..63048163d --- /dev/null +++ b/apps/assets/migrations/0119_auto_20221227_1740.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.16 on 2022-12-27 09:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0118_auto_20221227_1504'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='source', + field=models.CharField(default='local', max_length=30, verbose_name='Source'), + ), + migrations.DeleteModel( + name='GatheredUser', + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 6d9716c39..78e6e579d 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -7,7 +7,6 @@ from .gateway import * from .domain import * from .node import * from .utils import * -from .gathered_user import * from .favorite_asset import * from .account import * from .backup import * diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index 7703ac1e6..e0e87d37c 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords from common.utils import lazyproperty - +from ..const import AliasAccount, Source from .base import AbsConnectivity, BaseAccount __all__ = ['Account', 'AccountTemplate'] @@ -41,11 +41,6 @@ class AccountHistoricalRecords(HistoricalRecords): class Account(AbsConnectivity, BaseAccount): - class AliasAccount(models.TextChoices): - ALL = '@ALL', _('All') - INPUT = '@INPUT', _('Manual input') - USER = '@USER', _('Dynamic user') - asset = models.ForeignKey( 'assets.Asset', related_name='accounts', on_delete=models.CASCADE, verbose_name=_('Asset') @@ -56,6 +51,7 @@ class Account(AbsConnectivity, BaseAccount): ) version = models.IntegerField(default=0, verbose_name=_('Version')) history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version']) + source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source')) class Meta: verbose_name = _('Account') @@ -74,21 +70,32 @@ class Account(AbsConnectivity, BaseAccount): def platform(self): return self.asset.platform + @lazyproperty + def alias(self): + if self.username.startswith('@'): + return self.username + return self.name + def __str__(self): return '{}'.format(self.username) + @lazyproperty + def has_secret(self): + return bool(self.secret) + @classmethod def get_manual_account(cls): """ @INPUT 手动登录的账号(any) """ - return cls(name=cls.AliasAccount.INPUT.label, username=cls.AliasAccount.INPUT.value, secret=None) + return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None) @classmethod def get_user_account(cls, username): """ @USER 动态用户的账号(self) """ - return cls(name=cls.AliasAccount.USER.label, username=cls.AliasAccount.USER.value) + return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value) def get_su_from_accounts(self): - return self.asset.accounts.exclude(id=self.id) + """ 排除自己和以自己为 su-from 的账号 """ + return self.asset.accounts.exclude(id=self.id).exclude(su_from=self) class AccountTemplate(BaseAccount): diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 81693252d..6ef10b019 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -2,18 +2,16 @@ # -*- coding: utf-8 -*- # -import uuid import logging from collections import defaultdict from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty from orgs.mixins.models import OrgManager, JMSOrgBaseModel -from ..platform import Platform from ..base import AbsConnectivity +from ..platform import Platform __all__ = ['Asset', 'AssetQuerySet', 'default_node', 'Protocol'] logger = logging.getLogger(__name__) @@ -53,7 +51,7 @@ class NodesRelationMixin: NODES_CACHE_KEY = 'ASSET_NODES_{}' ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES' CACHE_TIME = 3600 * 24 * 7 - id = "" + id: str _all_nodes_keys = None def get_nodes(self): @@ -65,16 +63,29 @@ class NodesRelationMixin: def get_all_nodes(self, flat=False): from ..node import Node + node_keys = self.get_all_node_keys() + nodes = Node.objects.filter(key__in=node_keys).distinct() + if not flat: + return nodes + node_ids = set(nodes.values_list('id', flat=True)) + return node_ids + + def get_all_node_keys(self): node_keys = set() for node in self.get_nodes(): ancestor_keys = node.get_ancestor_keys(with_self=True) node_keys.update(ancestor_keys) - nodes = Node.objects.filter(key__in=node_keys).distinct() - if flat: - node_ids = set(nodes.values_list('id', flat=True)) - return node_ids - else: - return nodes + return node_keys + + @classmethod + def get_all_nodes_for_assets(cls, assets): + from ..node import Node + node_keys = set() + for asset in assets: + asset_node_keys = asset.get_all_node_keys() + node_keys.update(asset_node_keys) + nodes = Node.objects.filter(key__in=node_keys) + return nodes class Protocol(models.Model): @@ -87,7 +98,6 @@ class Protocol(models.Model): class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') @@ -97,7 +107,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) info = models.JSONField(verbose_name='Info', default=dict, blank=True) objects = AssetManager.from_queryset(AssetQuerySet)() diff --git a/apps/assets/models/automations/__init__.py b/apps/assets/models/automations/__init__.py index 82fa19620..abf23ed7e 100644 --- a/apps/assets/models/automations/__init__.py +++ b/apps/assets/models/automations/__init__.py @@ -5,4 +5,3 @@ from .gather_facts import * from .change_secret import * from .verify_account import * from .gather_accounts import * -from .discovery_account import * diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 9977f6830..6504e5898 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -1,25 +1,24 @@ import uuid + from celery import current_task from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.const.choices import Trigger -from common.mixins.models import CommonModelMixin -from common.db.fields import EncryptJsonDictTextField -from orgs.mixins.models import OrgModelMixin -from ops.mixin import PeriodTaskModelMixin +from assets.const import AutomationTypes from assets.models import Node, Asset from assets.tasks import execute_automation -from assets.const import AutomationTypes +from common.const.choices import Trigger +from common.db.fields import EncryptJsonDictTextField +from ops.mixin import PeriodTaskModelMixin +from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel -class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): +class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): accounts = models.JSONField(default=list, verbose_name=_("Accounts")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type')) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - comment = models.TextField(blank=True, verbose_name=_('Comment')) def __str__(self): return self.name + '@' + str(self.created_by) @@ -102,6 +101,12 @@ class AutomationExecution(OrgModelMixin): class Meta: verbose_name = _('Automation task execution') + permissions = [ + ('view_changesecretexecution', _('Can view change secret execution')), + ('add_changesecretexection', _('Can add change secret execution')), + ('view_gatheraccountsexecution', _('Can view gather accounts execution')), + ('add_gatheraccountsexecution', _('Can add gather accounts execution')), + ] @property def manager_type(self): diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index ecc0e98d4..7fb801cab 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -1,15 +1,15 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy from common.db import fields from common.db.models import JMSBaseModel -from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy from .base import BaseAutomation -__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord'] +__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin'] -class ChangeSecretAutomation(BaseAutomation): +class ChangeSecretMixin(models.Model): secret_type = models.CharField( choices=SecretType.choices, max_length=16, default=SecretType.PASSWORD, verbose_name=_('Secret type') @@ -24,6 +24,12 @@ class ChangeSecretAutomation(BaseAutomation): choices=SSHKeyStrategy.choices, max_length=16, default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') ) + + class Meta: + abstract = True + + +class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True) def save(self, *args, **kwargs): diff --git a/apps/assets/models/automations/discovery_account.py b/apps/assets/models/automations/discovery_account.py deleted file mode 100644 index 9e2adf610..000000000 --- a/apps/assets/models/automations/discovery_account.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ - -from .base import BaseAutomation - - -class DiscoveryAccountAutomation(BaseAutomation): - class Meta: - verbose_name = _("Discovery account automation") - - def to_attr_json(self): - attr_json = super().to_attr_json() - attr_json.update({ - 'type': 'discover_account' - }) - return attr_json diff --git a/apps/assets/models/automations/push_account.py b/apps/assets/models/automations/push_account.py index 8439041cb..33972eb58 100644 --- a/apps/assets/models/automations/push_account.py +++ b/apps/assets/models/automations/push_account.py @@ -1,12 +1,16 @@ +from django.db import models from django.utils.translation import ugettext_lazy as _ from assets.const import AutomationTypes from .base import BaseAutomation +from .change_secret import ChangeSecretMixin __all__ = ['PushAccountAutomation'] -class PushAccountAutomation(BaseAutomation): +class PushAccountAutomation(BaseAutomation, ChangeSecretMixin): + accounts = None + username = models.CharField(max_length=128, verbose_name=_('Username')) def save(self, *args, **kwargs): self.type = AutomationTypes.push_account diff --git a/apps/assets/models/backup.py b/apps/assets/models/backup.py index 3cf49a94d..d4a8b8cc8 100644 --- a/apps/assets/models/backup.py +++ b/apps/assets/models/backup.py @@ -7,26 +7,23 @@ from celery import current_task from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.models import OrgModelMixin -from ops.mixin import PeriodTaskModelMixin -from common.utils import get_logger from common.const.choices import Trigger from common.db.encoder import ModelJSONFieldEncoder -from common.mixins.models import CommonModelMixin +from common.utils import get_logger +from ops.mixin import PeriodTaskModelMixin +from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel __all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution'] logger = get_logger(__file__) -class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): types = models.JSONField(default=list) recipients = models.ManyToManyField( 'users.User', related_name='recipient_escape_route_plans', blank=True, verbose_name=_("Recipient") ) - comment = models.TextField(blank=True, verbose_name=_('Comment')) def __str__(self): return f'{self.name}({self.org_id})' diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 03697d427..8c8c08555 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -69,8 +69,6 @@ class BaseAccount(JMSOrgBaseModel): secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)() diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 9c70f2bb9..dd0d7b0d1 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -7,11 +7,6 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger -from users.models import User, UserGroup -from applications.models import Application -from ..models import SystemUser, Asset, Node - -from common.utils import lazyproperty, get_logger, get_object_or_none from orgs.mixins.models import OrgModelMixin logger = get_logger(__file__) diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 77758283d..4e2cfc71d 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- # -import uuid import random from django.db import models from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, lazyproperty -from orgs.mixins.models import OrgModelMixin - +from orgs.mixins.models import JMSOrgBaseModel from .gateway import Gateway logger = get_logger(__file__) @@ -16,11 +14,8 @@ logger = get_logger(__file__) __all__ = ['Domain'] -class Domain(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class Domain(JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) class Meta: verbose_name = _("Domain") @@ -51,5 +46,3 @@ class Domain(OrgModelMixin): @classmethod def get_gateway_queryset(cls): return Gateway.objects.all() - - diff --git a/apps/assets/models/favorite_asset.py b/apps/assets/models/favorite_asset.py index c5e6db484..8fbaeed64 100644 --- a/apps/assets/models/favorite_asset.py +++ b/apps/assets/models/favorite_asset.py @@ -2,13 +2,12 @@ # from django.db import models -from common.mixins.models import CommonModelMixin - +from common.db.models import JMSBaseModel __all__ = ['FavoriteAsset'] -class FavoriteAsset(CommonModelMixin): +class FavoriteAsset(JMSBaseModel): user = models.ForeignKey('users.User', on_delete=models.CASCADE) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE) diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py index 4664e11a8..f94ae826b 100644 --- a/apps/assets/models/gateway.py +++ b/apps/assets/models/gateway.py @@ -37,6 +37,7 @@ class Gateway(Host): class Meta: proxy = True + verbose_name = _("Gateway") def save(self, *args, **kwargs): self.platform = self.default_platform() diff --git a/apps/assets/models/gathered_user.py b/apps/assets/models/gathered_user.py deleted file mode 100644 index 3c0a743b9..000000000 --- a/apps/assets/models/gathered_user.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.models import OrgModelMixin - -__all__ = ['GatheredUser'] - - -class GatheredUser(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) - username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) - present = models.BooleanField(default=True, verbose_name=_("Present")) - date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login")) - ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login")) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) - date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) - - @property - def name(self): - return self.asset.name - - @property - def ip(self): - return self.asset.address - - class Meta: - verbose_name = _('GatherUser') - ordering = ['asset'] - - def __str__(self): - return '{}: {}'.format(self.asset.name, self.username) - - - diff --git a/apps/assets/models/label.py b/apps/assets/models/label.py index afad1f069..3aff63385 100644 --- a/apps/assets/models/label.py +++ b/apps/assets/models/label.py @@ -4,6 +4,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.utils import lazyproperty from orgs.mixins.models import JMSOrgBaseModel @@ -19,7 +20,6 @@ class Label(JMSOrgBaseModel): category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, default=USER_CATEGORY, verbose_name=_("Category")) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) @classmethod def get_queryset_group_by_name(cls): @@ -27,6 +27,10 @@ class Label(JMSOrgBaseModel): for name in names: yield name, cls.objects.filter(name=name) + @lazyproperty + def asset_count(self): + return self.assets.count() + def __str__(self): return "{}:{}".format(self.name, self.value) diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 54bdecf61..626f931d0 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -1,29 +1,24 @@ # -*- coding: utf-8 -*- # import re -import time -import uuid import threading -import os import time import uuid - from collections import defaultdict + +from django.core.cache import cache from django.db import models, transaction from django.db.models import Q, Manager -from django.db.utils import IntegrityError -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import ugettext from django.db.transaction import atomic -from django.core.cache import cache +from django.utils.translation import ugettext +from django.utils.translation import ugettext_lazy as _ -from common.utils.lock import DistributedLock -from common.utils.common import timeit from common.db.models import output_as_string from common.utils import get_logger -from orgs.mixins.models import OrgModelMixin, OrgManager -from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org +from common.utils.lock import DistributedLock +from orgs.mixins.models import OrgManager, JMSOrgBaseModel from orgs.models import Organization +from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org __all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet'] logger = get_logger(__name__) @@ -178,9 +173,7 @@ class FamilyMixin: return parent_keys def get_ancestor_keys(self, with_self=False): - return self.get_node_ancestor_keys( - self.key, with_self=with_self - ) + return self.get_node_ancestor_keys(self.key, with_self=with_self) @property def ancestors(self): @@ -437,6 +430,12 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin): assets = Asset.objects.filter(nodes=self) return assets.distinct() + def get_assets_for_tree(self): + return self.get_assets().only( + "id", "name", "address", "platform_id", + "org_id", "is_active" + ).prefetch_related('platform') + def get_valid_assets(self): return self.get_assets().valid() @@ -547,7 +546,7 @@ class SomeNodesMixin: return root_nodes -class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): +class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' value = models.CharField(max_length=128, verbose_name=_("Value")) diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index 64aeb2da3..c013dd2d6 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -2,9 +2,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from assets.const import AllTypes -from common.db.fields import JsonDictTextField - from assets.const import Protocol +from common.db.fields import JsonDictTextField __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] @@ -45,8 +44,6 @@ class PlatformAutomation(models.Model): ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method")) gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")) - push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled")) - push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method")) change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled")) change_secret_method = models.TextField( max_length=32, blank=True, null=True, verbose_name=_("Change password method")) @@ -83,7 +80,7 @@ class Platform(models.Model): protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled")) # 账号有关的 su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled")) - su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("SU method")) + su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method")) automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform', blank=True, null=True, verbose_name=_("Automation")) diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index d60c5a28c..891c5b9d6 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import validate_ssh_private_key - __all__ = [ 'private_key_validator', ] @@ -22,8 +21,6 @@ def private_key_validator(value): def update_internal_platforms(platform_model): - from assets.const import AllTypes - platforms = [ {'name': 'Linux', 'category': 'host', 'type': 'linux'}, {'name': 'BSD', 'category': 'host', 'type': 'unix'}, @@ -32,7 +29,6 @@ def update_internal_platforms(platform_model): {'name': 'Windows', 'category': 'host', 'type': 'unix'}, { 'name': 'AIX', 'category': 'host', 'type': 'unix', - 'push_account_method': 'create_account_aix', 'change_secret_method': 'change_secret_aix', }, {'name': 'Windows', 'category': 'host', 'type': 'windows'}, diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 7d82131a7..f70a94bbf 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -6,7 +6,6 @@ from .label import * from .node import * from .gateway import * from .domain import * -from .gathered_user import * from .favorite_asset import * from .account import * from .platform import * diff --git a/apps/assets/serializers/account/account.py b/apps/assets/serializers/account/account.py index dfad8590f..c2eb786dd 100644 --- a/apps/assets/serializers/account/account.py +++ b/apps/assets/serializers/account/account.py @@ -1,15 +1,15 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.serializers import SecretReadableMixin -from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from assets.tasks import push_accounts_to_assets +from assets.const import SecretType, Source from assets.models import Account, AccountTemplate, Asset +from assets.tasks import push_accounts_to_assets +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.drf.serializers import SecretReadableMixin, BulkModelSerializer from .base import BaseAccountSerializer -from assets.const import SecretType -class AccountSerializerCreateMixin(serializers.ModelSerializer): +class AccountSerializerCreateMixin(BulkModelSerializer): template = serializers.UUIDField( required=False, allow_null=True, write_only=True, label=_('Account template') @@ -53,35 +53,47 @@ class AccountSerializerCreateMixin(serializers.ModelSerializer): return instance +class AccountAssetSerializer(serializers.ModelSerializer): + platform = ObjectRelatedField(read_only=True) + + class Meta: + model = Asset + fields = ['id', 'name', 'address', 'platform'] + + def to_internal_value(self, data): + if isinstance(data, dict): + i = data.get('id') + else: + i = data + + try: + return Asset.objects.get(id=i) + except Asset.DoesNotExist: + raise serializers.ValidationError(_('Asset not found')) + + class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): - asset = ObjectRelatedField( - required=False, queryset=Asset.objects, - label=_('Asset'), attrs=('id', 'name', 'address', 'platform_id') - ) + asset = AccountAssetSerializer(label=_('Asset')) + source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True) su_from = ObjectRelatedField( - required=False, queryset=Account.objects, - label=_('Account'), attrs=('id', 'name', 'username') + required=False, queryset=Account.objects, allow_null=True, allow_empty=True, + label=_('Su from'), attrs=('id', 'name', 'username') ) class Meta(BaseAccountSerializer.Meta): model = Account fields = BaseAccountSerializer.Meta.fields \ - + ['su_from', 'version', 'asset'] \ - + ['template', 'push_now'] + + ['su_from', 'version', 'asset'] \ + + ['template', 'push_now', 'source'] extra_kwargs = { **BaseAccountSerializer.Meta.extra_kwargs, 'name': {'required': False, 'allow_null': True}, } - def __init__(self, *args, data=None, **kwargs): - super().__init__(*args, data=data, **kwargs) - if data and 'name' not in data: - username = data.get('username') - if username is not None: - data['name'] = username - if hasattr(self, 'initial_data') and \ - not getattr(self, 'initial_data', None): - delattr(self, 'initial_data') + def validate_name(self, value): + if not value: + value = self.initial_data.get('username') + return value @classmethod def setup_eager_loading(cls, queryset): diff --git a/apps/assets/serializers/account/backup.py b/apps/assets/serializers/account/backup.py index 06cf4e2f9..34121dadd 100644 --- a/apps/assets/serializers/account/backup.py +++ b/apps/assets/serializers/account/backup.py @@ -34,7 +34,6 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): - trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode')) class Meta: model = AccountBackupPlanExecution diff --git a/apps/assets/serializers/account/base.py b/apps/assets/serializers/account/base.py index c0e3553e8..becfd4df9 100644 --- a/apps/assets/serializers/account/base.py +++ b/apps/assets/serializers/account/base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers from assets.models import BaseAccount from assets.serializers.base import AuthValidateMixin @@ -9,6 +10,8 @@ __all__ = ['BaseAccountSerializer'] class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): + has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) + class Meta: model = BaseAccount fields_mini = ['id', 'name', 'username'] diff --git a/apps/assets/serializers/account/template.py b/apps/assets/serializers/account/template.py index 7a7de7f11..9227b5585 100644 --- a/apps/assets/serializers/account/template.py +++ b/apps/assets/serializers/account/template.py @@ -1,5 +1,5 @@ -from common.drf.serializers import SecretReadableMixin from assets.models import AccountTemplate +from common.drf.serializers import SecretReadableMixin from .base import BaseAccountSerializer diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index 188539981..e2a84fe5a 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -1,21 +1,22 @@ # -*- coding: utf-8 -*- # -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ -from django.db.transaction import atomic from django.db.models import F +from django.db.transaction import atomic +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -from common.drf.serializers import WritableNestedModelSerializer from common.drf.fields import LabeledChoiceField, ObjectRelatedField -from orgs.mixins.serializers import OrgResourceSerializerMixin +from common.drf.serializers import WritableNestedModelSerializer +from orgs.mixins.serializers import BulkOrgResourceSerializerMixin from ..account import AccountSerializer -from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol from ...const import Category, AllTypes +from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', + 'AssetDetailSerializer', ] @@ -58,7 +59,7 @@ class AssetAccountSerializer(AccountSerializer): fields = fields_mini + fields_write_only -class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer): +class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSerializer): category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) domain = ObjectRelatedField(required=False, queryset=Domain.objects, label=_('Domain'), allow_null=True) @@ -66,49 +67,26 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer) nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) labels = AssetLabelSerializer(many=True, required=False, label=_('Labels')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) - accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) - enabled_info = serializers.SerializerMethodField() + accounts = AssetAccountSerializer(many=True, required=False, label=_('Account')) class Meta: model = Asset - fields_mini = ['id', 'name', 'address', 'enabled_info'] + fields_mini = ['id', 'name', 'address'] fields_small = fields_mini + ['is_active', 'comment'] fields_fk = ['domain', 'platform', 'platform'] fields_m2m = [ 'nodes', 'labels', 'protocols', 'accounts', 'nodes_display', ] read_only_fields = [ - 'category', 'type', 'specific', 'info', - 'connectivity', 'date_verified', 'created_by', - 'date_created' + 'category', 'type', 'info', + 'connectivity', 'date_verified', + 'created_by', 'date_created' ] fields = fields_small + fields_fk + fields_m2m + read_only_fields extra_kwargs = { 'name': {'label': _("Name")}, 'address': {'label': _('Address')}, - } - - def get_field_names(self, declared_fields, info): - names = super().get_field_names(declared_fields, info) - if self.__class__.__name__ != 'AssetSerializer': - names.remove('specific') - return names - - @staticmethod - def get_enabled_info(obj): - platform = obj.platform - automation = platform.automation - return { - 'su_enabled': platform.su_enabled, - 'ping_enabled': automation.ping_enabled, - 'domain_enabled': platform.domain_enabled, - 'ansible_enabled': automation.ansible_enabled, - 'protocols_enabled': platform.protocols_enabled, - 'gather_facts_enabled': automation.gather_facts_enabled, - 'push_account_enabled': automation.push_account_enabled, - 'change_secret_enabled': automation.change_secret_enabled, - 'verify_account_enabled': automation.verify_account_enabled, - 'gather_accounts_enabled': automation.gather_accounts_enabled, + 'nodes_display': {'label': _('Node path')}, } @classmethod @@ -117,7 +95,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer) queryset = queryset.prefetch_related('domain', 'platform', 'protocols') \ .annotate(category=F("platform__category")) \ .annotate(type=F("platform__type")) - queryset = queryset.prefetch_related('nodes', 'labels') + queryset = queryset.prefetch_related('nodes', 'labels', 'accounts') return queryset def perform_nodes_display_create(self, instance, nodes_display): @@ -188,6 +166,30 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer) return instance +class AssetDetailSerializer(AssetSerializer): + accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) + enabled_info = serializers.SerializerMethodField() + + class Meta(AssetSerializer.Meta): + fields = AssetSerializer.Meta.fields + ['accounts', 'enabled_info', 'info', 'specific'] + + @staticmethod + def get_enabled_info(obj): + platform = obj.platform + automation = platform.automation + return { + 'su_enabled': platform.su_enabled, + 'ping_enabled': automation.ping_enabled, + 'domain_enabled': platform.domain_enabled, + 'ansible_enabled': automation.ansible_enabled, + 'protocols_enabled': platform.protocols_enabled, + 'gather_facts_enabled': automation.gather_facts_enabled, + 'change_secret_enabled': automation.change_secret_enabled, + 'verify_account_enabled': automation.verify_account_enabled, + 'gather_accounts_enabled': automation.gather_accounts_enabled, + } + + class MiniAssetSerializer(serializers.ModelSerializer): class Meta: model = Asset diff --git a/apps/assets/serializers/asset/web.py b/apps/assets/serializers/asset/web.py index 333795473..aa35e28e0 100644 --- a/apps/assets/serializers/asset/web.py +++ b/apps/assets/serializers/asset/web.py @@ -1,4 +1,3 @@ - from assets.models import Web from .common import AssetSerializer @@ -19,12 +18,12 @@ class WebSerializer(AssetSerializer): 'label': 'URL' }, 'username_selector': { - 'default': 'input[type=text]' + 'default': 'name=username' }, 'password_selector': { - 'default': 'input[type=password]' + 'default': 'name=password' }, 'submit_selector': { - 'default': 'button[type=submit]', + 'default': 'id=longin_button', }, } diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py deleted file mode 100644 index a0b58de45..000000000 --- a/apps/assets/serializers/gathered_user.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import OrgResourceModelSerializerMixin -from common.drf.fields import ObjectRelatedField -from ..models import GatheredUser, Asset - - -class GatheredUserSerializer(OrgResourceModelSerializerMixin): - asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset')) - - class Meta: - model = GatheredUser - fields_mini = ['id'] - fields_small = fields_mini + [ - 'username', 'ip_last_login', 'present', 'name', - 'date_last_login', 'date_created', 'date_updated' - ] - fields_fk = ['asset', 'ip'] - fields = fields_small + fields_fk - read_only_fields = fields - extra_kwargs = { - 'name': {'label': _("Hostname")}, - 'ip': {'label': 'IP'}, - } diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 450b13a44..6992f4e8d 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -1,25 +1,22 @@ # -*- coding: utf-8 -*- # -from rest_framework import serializers +from django.db.models import Count from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers from orgs.mixins.serializers import BulkOrgResourceModelSerializer - from ..models import Label class LabelSerializer(BulkOrgResourceModelSerializer): - asset_count = serializers.SerializerMethodField(label=_("Assets amount")) - category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) + asset_count = serializers.ReadOnlyField(label=_("Assets amount")) class Meta: model = Label fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'value', 'category', 'category_display', - 'is_active', - 'date_created', - 'comment', + 'value', 'category', 'is_active', + 'date_created', 'comment', ] fields_m2m = ['asset_count', 'assets'] fields = fields_small + fields_m2m @@ -30,14 +27,10 @@ class LabelSerializer(BulkOrgResourceModelSerializer): 'assets': {'required': False, 'label': _('Asset')} } - @staticmethod - def get_asset_count(obj): - return obj.assets.count() - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['get_category_display']) - return fields + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.annotate(asset_count=Count('assets')) + return queryset class LabelDistinctSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index ccb536bb2..ab72cecc9 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -25,7 +25,7 @@ class ProtocolSettingSerializer(serializers.Serializer): sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home")) # HTTP - auto_fill = serializers.BooleanField(default=False, label=_("Auto fill")) + autofile = serializers.BooleanField(default=False, label=_("Autofill")) username_selector = serializers.CharField( default="", allow_blank=True, label=_("Username selector") ) @@ -38,37 +38,26 @@ class ProtocolSettingSerializer(serializers.Serializer): class PlatformAutomationSerializer(serializers.ModelSerializer): - class Meta: model = PlatformAutomation fields = [ "id", - "ansible_enabled", - "ansible_config", - "ping_enabled", - "ping_method", - "gather_facts_enabled", - "gather_facts_method", - "push_account_enabled", - "push_account_method", - "change_secret_enabled", - "change_secret_method", - "verify_account_enabled", - "verify_account_method", - "gather_accounts_enabled", - "gather_accounts_method", + "ansible_enabled", "ansible_config", + "ping_enabled", "ping_method", + "gather_facts_enabled", "gather_facts_method", + "change_secret_enabled", "change_secret_method", + "verify_account_enabled", "verify_account_method", + "gather_accounts_enabled", "gather_accounts_method", ] extra_kwargs = { "ping_enabled": {"label": "启用资产探测"}, - "ping_method": {"label": "探测方式"}, - "gather_facts_enabled": {"label": "启用收集信息"}, + "ping_method": {"label": "资产探测方式"}, + "gather_facts_enabled": {"label": "收集资产信息"}, "gather_facts_method": {"label": "收集信息方式"}, "verify_account_enabled": {"label": "启用校验账号"}, "verify_account_method": {"label": "校验账号方式"}, - "push_account_enabled": {"label": "启用推送账号"}, - "push_account_method": {"label": "推送账号方式"}, "change_secret_enabled": {"label": "启用账号改密"}, - "change_secret_method": {"label": "账号创建改密方式"}, + "change_secret_method": {"label": "账号改密方式"}, "gather_accounts_enabled": {"label": "启用账号收集"}, "gather_accounts_method": {"label": "收集账号方式"}, } @@ -81,13 +70,8 @@ class PlatformProtocolsSerializer(serializers.ModelSerializer): class Meta: model = PlatformProtocol fields = [ - "id", - "name", - "port", - "primary", - "default", - "required", - "secret_types", + "id", "name", "port", "primary", + "default", "required", "secret_types", "setting", ] @@ -113,17 +97,12 @@ class PlatformSerializer(WritableNestedModelSerializer): model = Platform fields_mini = ["id", "name", "internal"] fields_small = fields_mini + [ - "category", - "type", - "charset", + "category", "type", "charset", ] fields = fields_small + [ - "protocols_enabled", - "protocols", - "domain_enabled", - "su_enabled", - "su_method", - "automation", + "protocols_enabled", "protocols", + "domain_enabled", "su_enabled", + "su_method", "automation", "comment", ] extra_kwargs = { diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index 5aac26319..86e6a0a9a 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -103,6 +103,7 @@ def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): # m2m_model.objects.bulk_create(to_create) # + RELATED_NODE_IDS = '_related_node_ids' diff --git a/apps/assets/signal_handlers/node_assets_mapping.py b/apps/assets/signal_handlers/node_assets_mapping.py index b242f3be8..27640fc76 100644 --- a/apps/assets/signal_handlers/node_assets_mapping.py +++ b/apps/assets/signal_handlers/node_assets_mapping.py @@ -20,13 +20,9 @@ logger = get_logger(__file__) # ------------------------------------ -def get_node_assets_mapping_for_memory_pub_sub(): - return RedisPubSub('fm.node_all_asset_ids_memory_mapping') - - class NodeAssetsMappingForMemoryPubSub(LazyObject): def _setup(self): - self._wrapped = get_node_assets_mapping_for_memory_pub_sub() + self._wrapped = RedisPubSub('fm.node_all_asset_ids_memory_mapping') node_assets_mapping_for_memory_pub_sub = NodeAssetsMappingForMemoryPubSub() diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 773f5c348..dd42c44eb 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -23,13 +23,13 @@ router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') -router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') -router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution') +router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') +router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-execution') router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation') @@ -50,7 +50,6 @@ urlpatterns = [ name='account-secret-history'), path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'), - path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'), path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'), path('nodes//children/', api.NodeChildrenApi.as_view(), name='node-children'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), diff --git a/apps/assets/utils/__init__.py b/apps/assets/utils/__init__.py new file mode 100644 index 000000000..9f588b6d2 --- /dev/null +++ b/apps/assets/utils/__init__.py @@ -0,0 +1,2 @@ +from .k8s import * +from .node import * diff --git a/apps/assets/utils/k8s.py b/apps/assets/utils/k8s.py new file mode 100644 index 000000000..5ffd1612d --- /dev/null +++ b/apps/assets/utils/k8s.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +from urllib3.exceptions import MaxRetryError +from urllib.parse import urlencode + +from kubernetes import client +from kubernetes.client import api_client +from kubernetes.client.api import core_v1_api +from kubernetes.client.exceptions import ApiException + +from common.utils import get_logger + +from ..const import CloudTypes, Category + +logger = get_logger(__file__) + + +class KubernetesClient: + def __init__(self, url, token, proxy=None): + self.url = url + self.token = token + self.proxy = proxy + + def get_api(self): + configuration = client.Configuration() + configuration.host = self.url + configuration.proxy = self.proxy + configuration.verify_ssl = False + configuration.api_key = {"authorization": "Bearer " + self.token} + c = api_client.ApiClient(configuration=configuration) + api = core_v1_api.CoreV1Api(c) + return api + + def get_namespace_list(self): + api = self.get_api() + namespace_list = [] + for ns in api.list_namespace().items: + namespace_list.append(ns.metadata.name) + return namespace_list + + def get_services(self): + api = self.get_api() + ret = api.list_service_for_all_namespaces(watch=False) + for i in ret.items: + print("%s \t%s \t%s \t%s \t%s \n" % ( + i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports)) + + def get_pod_info(self, namespace, pod): + api = self.get_api() + resp = api.read_namespaced_pod(namespace=namespace, name=pod) + return resp + + def get_pod_logs(self, namespace, pod): + api = self.get_api() + log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200) + return log_content + + def get_pods(self): + api = self.get_api() + try: + ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3)) + except MaxRetryError: + logger.warning('Kubernetes connection timed out') + return + except ApiException as e: + if e.status == 401: + logger.warning('Kubernetes User not authenticated') + else: + logger.warning(e) + return + data = {} + for i in ret.items: + namespace = i.metadata.namespace + pod_info = { + 'pod_name': i.metadata.name, + 'containers': [j.name for j in i.spec.containers] + } + if namespace in data: + data[namespace].append(pod_info) + else: + data[namespace] = [pod_info, ] + return data + + @classmethod + def get_proxy_url(cls, asset): + if not asset.domain: + return None + + gateway = asset.domain.select_gateway() + if not gateway: + return None + return f'{gateway.address}:{gateway.port}' + + @classmethod + def get_kubernetes_data(cls, asset, secret): + k8s_url = f'{asset.address}:{asset.port}' + proxy_url = cls.get_proxy_url(asset) + k8s = cls(k8s_url, secret, proxy=proxy_url) + return k8s.get_pods() + + +class KubernetesTree: + def __init__(self, asset, secret): + self.asset = asset + self.secret = secret + + def as_asset_tree_node(self): + i = str(self.asset.id) + name = str(self.asset) + node = self.create_tree_node( + i, i, name, 'asset', is_open=True, + ) + return node + + def as_namespace_node(self, name, tp, counts=0): + i = urlencode({'namespace': name}) + pid = str(self.asset.id) + name = f'{name}({counts})' + node = self.create_tree_node(i, pid, name, tp, icon='cloud') + return node + + def as_pod_tree_node(self, namespace, name, tp, counts=0): + pid = urlencode({'namespace': namespace}) + i = urlencode({'namespace': namespace, 'pod': name}) + name = f'{name}({counts})' + node = self.create_tree_node(i, pid, name, tp, icon='cloud') + return node + + def as_container_tree_node(self, namespace, pod, name, tp): + pid = urlencode({'namespace': namespace, 'pod': pod}) + i = urlencode({'namespace': namespace, 'pod': pod, 'container': name}) + node = self.create_tree_node( + i, pid, name, tp, icon='cloud', is_container=True + ) + return node + + @staticmethod + def create_tree_node(id_, pid, name, identity, icon='', is_container=False, is_open=False): + node = { + 'id': id_, + 'name': name, + 'title': name, + 'pId': pid, + 'isParent': not is_container, + 'open': is_open, + 'iconSkin': icon, + 'meta': { + 'type': 'k8s', + 'data': { + 'category': Category.CLOUD, + 'type': CloudTypes.K8S, + 'identity': identity + } + } + } + return node + + def async_tree_node(self, namespace, pod): + tree = [] + data = KubernetesClient.get_kubernetes_data(self.asset, self.secret) + if not data: + return tree + + if pod: + for container in next( + filter( + lambda x: x['pod_name'] == pod, data[namespace] + ) + )['containers']: + container_node = self.as_container_tree_node( + namespace, pod, container, 'container' + ) + tree.append(container_node) + elif namespace: + for pod in data[namespace]: + pod_nodes = self.as_pod_tree_node( + namespace, pod['pod_name'], 'pod', len(pod['containers']) + ) + tree.append(pod_nodes) + else: + for namespace, pods in data.items(): + namespace_node = self.as_namespace_node( + namespace, 'namespace', len(pods) + ) + tree.append(namespace_node) + return tree diff --git a/apps/assets/utils.py b/apps/assets/utils/node.py similarity index 98% rename from apps/assets/utils.py rename to apps/assets/utils/node.py index 6c39fffa5..5d22421c5 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils/node.py @@ -7,8 +7,8 @@ from common.struct import Stack from common.db.models import output_as_string from orgs.utils import ensure_in_real_or_default_org, current_org -from .locks import NodeTreeUpdateLock -from .models import Node, Asset +from ..locks import NodeTreeUpdateLock +from ..models import Node, Asset logger = get_logger(__file__) diff --git a/apps/audits/api.py b/apps/audits/api.py index 397c96c6b..b060a0eb1 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -2,30 +2,31 @@ # from importlib import import_module -from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin -from django.db.models import F, Value -from django.db.models.functions import Concat from django.conf import settings -from rest_framework.permissions import IsAuthenticated from rest_framework import generics +from rest_framework.permissions import IsAuthenticated +from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin -from common.drf.api import JMSReadOnlyModelViewSet -from common.plugins.es import QuerySet as ESQuerySet -from common.drf.filters import DatetimeRangeFilter +from ops.models.job import JobAuditLog from common.api import CommonGenericViewSet -from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin +from common.drf.filters import DatetimeRangeFilter +from common.plugins.es import QuerySet as ESQuerySet from orgs.utils import current_org -# from ops.models import CommandExecution -from . import filters +from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet from .backends import TYPE_ENGINE_MAPPING from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog -from .serializers import FTPLogSerializer, UserLoginLogSerializer +from .serializers import FTPLogSerializer, UserLoginLogSerializer, JobAuditLogSerializer from .serializers import ( - OperateLogSerializer, OperateLogActionDetailSerializer, - PasswordChangeLogSerializer + OperateLogSerializer, OperateLogActionDetailSerializer, PasswordChangeLogSerializer ) +class JobAuditViewSet(OrgBulkModelViewSet): + model = JobAuditLog + serializer_class = JobAuditLogSerializer + http_method_names = ('get', 'head', 'options') + + class FTPLogViewSet(CreateModelMixin, ListModelMixin, OrgGenericViewSet): model = FTPLog serializer_class = FTPLogSerializer @@ -33,7 +34,7 @@ class FTPLogViewSet(CreateModelMixin, ListModelMixin, OrgGenericViewSet): date_range_filter_fields = [ ('date_start', ('date_from', 'date_to')) ] - filterset_fields = ['user', 'asset', 'system_user', 'filename'] + filterset_fields = ['user', 'asset', 'account', 'filename'] search_fields = filterset_fields ordering = ['-date_start'] diff --git a/apps/audits/const.py b/apps/audits/const.py index 62edc62a2..6987de94f 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -17,12 +17,10 @@ MODELS_NEED_RECORD = ( "LoginAssetACL", "LoginConfirmSetting", # assets - 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', + 'Asset', 'Node', 'Domain', 'Gateway', 'CommandFilterRule', 'CommandFilter', 'Platform', 'Label', - # applications - 'Application', # account - 'AuthBook', + 'Account', # orgs "Organization", # settings @@ -36,8 +34,7 @@ MODELS_NEED_RECORD = ( # rbac 'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding', # xpack - 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', - 'GatherUserTask', 'Interface', + 'License', 'Account', 'SyncInstanceTask', 'Interface', ) diff --git a/apps/audits/migrations/0017_auto_20221220_1757.py b/apps/audits/migrations/0017_auto_20221220_1757.py new file mode 100644 index 000000000..b879648e8 --- /dev/null +++ b/apps/audits/migrations/0017_auto_20221220_1757.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-12-20 09:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('audits', '0016_auto_20221111_1919'), + ] + + operations = [ + migrations.RenameField( + model_name='ftplog', + old_name='system_user', + new_name='account', + ), + migrations.AlterField( + model_name='ftplog', + name='account', + field=models.CharField(default='', max_length=128, verbose_name='Account'), + preserve_default=False, + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 1e65ea233..5f11fca9f 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -2,11 +2,11 @@ import uuid from django.db import models from django.db.models import Q -from django.utils.translation import gettext, ugettext_lazy as _ from django.utils import timezone +from django.utils.translation import gettext, ugettext_lazy as _ -from common.utils import lazyproperty from common.db.encoder import ModelJSONFieldEncoder +from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org from .const import ( @@ -32,7 +32,7 @@ class FTPLog(OrgModelMixin): max_length=128, verbose_name=_("Remote addr"), blank=True, null=True ) asset = models.CharField(max_length=1024, verbose_name=_("Asset")) - system_user = models.CharField(max_length=128, verbose_name=_("System user")) + account = models.CharField(max_length=128, verbose_name=_("Account")) operate = models.CharField( max_length=16, verbose_name=_("Operate"), choices=OperateChoices.choices ) diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 10e5a7f16..f5aee2f65 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -4,17 +4,28 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.drf.fields import LabeledChoiceField +from ops.models.job import JobAuditLog +from ops.serializers.job import JobExecutionSerializer from terminal.models import Session from . import models from .const import ( - ActionChoices, - OperateChoices, - MFAChoices, - LoginStatusChoices, + ActionChoices, OperateChoices, + MFAChoices, LoginStatusChoices, LoginTypeChoices, ) +class JobAuditLogSerializer(JobExecutionSerializer): + class Meta: + model = JobAuditLog + read_only_fields = [ + "id", "material", "time_cost", 'date_start', + 'date_finished', 'date_created', + 'is_finished', 'is_success', 'created_by', + ] + fields = read_only_fields + [] + + class FTPLogSerializer(serializers.ModelSerializer): operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate")) @@ -22,14 +33,8 @@ class FTPLogSerializer(serializers.ModelSerializer): model = models.FTPLog fields_mini = ["id"] fields_small = fields_mini + [ - "user", - "remote_addr", - "asset", - "system_user", - "org_id", - "operate", - "filename", - "is_success", + "user", "remote_addr", "asset", "account", + "org_id", "operate", "filename", "is_success", "date_start", ] fields = fields_small @@ -44,18 +49,11 @@ class UserLoginLogSerializer(serializers.ModelSerializer): model = models.UserLoginLog fields_mini = ["id"] fields_small = fields_mini + [ - "username", - "type", - "ip", - "city", - "user_agent", - "mfa", - "reason", - "reason_display", - "backend", - "backend_display", - "status", - "datetime", + "username", "type", "ip", + "city", "user_agent", "mfa", + "reason", "reason_display", + "backend", "backend_display", + "status", "datetime", ] fields = fields_small extra_kwargs = { @@ -78,14 +76,9 @@ class OperateLogSerializer(serializers.ModelSerializer): model = models.OperateLog fields_mini = ["id"] fields_small = fields_mini + [ - "user", - "action", - "resource_type", - "resource_type_display", - "resource", - "remote_addr", - "datetime", - "org_id", + "user", "action", "resource_type", + "resource_type_display", "resource", + "remote_addr", "datetime", "org_id", ] fields = fields_small extra_kwargs = {"resource_type_display": {"label": _("Resource Type")}} @@ -101,44 +94,3 @@ class SessionAuditSerializer(serializers.ModelSerializer): class Meta: model = Session fields = "__all__" - - -# -# class CommandExecutionSerializer(serializers.ModelSerializer): -# is_success = serializers.BooleanField(read_only=True, label=_('Is success')) -# hosts_display = serializers.ListSerializer( -# child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display') -# ) -# -# class Meta: -# model = CommandExecution -# fields_mini = ['id'] -# fields_small = fields_mini + [ -# 'command', 'is_finished', 'user', -# 'date_start', 'result', 'is_success', 'org_id' -# ] -# fields = fields_small + ['hosts', 'hosts_display', 'user_display'] -# extra_kwargs = { -# 'result': {'label': _('Result')}, # model 上的方法,只能在这修改 -# 'is_success': {'label': _('Is success')}, -# 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 -# 'user': {'label': _('User')}, -# 'user_display': {'label': _('User display')}, -# } -# -# @classmethod -# def setup_eager_loading(cls, queryset): -# """ Perform necessary eager loading of data. """ -# queryset = queryset.prefetch_related('user', 'hosts') -# return queryset -# -# -# class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.ModelSerializer): -# asset_display = serializers.ReadOnlyField() -# commandexecution_display = serializers.ReadOnlyField() -# -# class Meta: -# model = CommandExecution.hosts.through -# fields = [ -# 'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display' -# ] diff --git a/apps/audits/urls/api_urls.py b/apps/audits/urls/api_urls.py index 902c65fbf..fa8ee63fc 100644 --- a/apps/audits/urls/api_urls.py +++ b/apps/audits/urls/api_urls.py @@ -7,7 +7,6 @@ from rest_framework.routers import DefaultRouter from common import api as capi from .. import api - app_name = "audits" router = DefaultRouter() @@ -15,9 +14,7 @@ router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log') router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log') router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log') router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log') -# router.register(r'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log') -# router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation') - +router.register(r'job-logs', api.JobAuditViewSet, 'job-log') urlpatterns = [ path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'), diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 97ef3b5ff..782346fc5 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -19,12 +19,12 @@ from common.utils import random_string from common.utils.django import get_request_os from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices -from terminal.const import NativeClient, TerminalType -from terminal.models import EndpointRule, Applet +from terminal.connect_methods import NativeClient, ConnectMethodUtil +from terminal.models import EndpointRule from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, + SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -34,30 +34,6 @@ class RDPFileClientProtocolURLMixin: request: Request get_serializer: callable - @staticmethod - def set_applet_info(token, rdp_options): - # remote-app - applet = Applet.objects.filter(name=token.connect_method).first() - if not applet: - return rdp_options - - cmdline = { - 'app_name': applet.name, - 'user_id': str(token.user.id), - 'asset_id': str(token.asset.id), - 'token_id': str(token.id) - } - - app = '||tinker' - rdp_options['remoteapplicationmode:i'] = '1' - rdp_options['alternate shell:s'] = app - rdp_options['remoteapplicationprogram:s'] = app - rdp_options['remoteapplicationname:s'] = app - - cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode() - rdp_options['remoteapplicationcmdline:s'] = cmdline_b64 - return rdp_options - def get_rdp_file_info(self, token: ConnectionToken): rdp_options = { 'full address:s': '', @@ -114,8 +90,10 @@ class RDPFileClientProtocolURLMixin: rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') - # 设置远程应用 - self.set_applet_info(token, rdp_options) + # 设置远程应用, 不是 Mstsc + if token.connect_method != NativeClient.mstsc: + remote_app_options = token.get_remote_app_option() + rdp_options.update(remote_app_options) # 文件名 name = token.asset.name @@ -145,7 +123,7 @@ class RDPFileClientProtocolURLMixin: _os = get_request_os(self.request) connect_method_name = token.connect_method - connect_method_dict = TerminalType.get_connect_method( + connect_method_dict = ConnectMethodUtil.get_connect_method( token.connect_method, token.protocol, _os ) if connect_method_dict is None: @@ -159,15 +137,17 @@ class RDPFileClientProtocolURLMixin: 'file': {} } - if connect_method_name == NativeClient.mstsc: + if connect_method_name == NativeClient.mstsc or connect_method_dict['type'] == 'applet': filename, content = self.get_rdp_file_info(token) data.update({ + 'protocol': 'rdp', 'file': { 'name': filename, 'content': content, } }) else: + print("Connect method: {}".format(connect_method_dict)) endpoint = self.get_smart_endpoint( protocol=connect_method_dict['endpoint_protocol'], asset=token.asset @@ -227,38 +207,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView search_fields = filterset_fields serializer_classes = { 'default': ConnectionTokenSerializer, - 'get_secret_detail': ConnectionTokenSecretSerializer, } rbac_perms = { 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', 'create': 'authentication.add_connectiontoken', 'expire': 'authentication.add_connectiontoken', - 'get_secret_detail': 'authentication.view_connectiontokensecret', 'get_rdp_file': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken', } - @action(methods=['POST'], detail=False, url_path='secret') - def get_secret_detail(self, request, *args, **kwargs): - """ 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """ - rbac_perm = 'authentication.view_connectiontokensecret' - if not request.user.has_perm(rbac_perm): - raise PermissionDenied('Not allow to view secret') - - token_id = request.data.get('id') or '' - token = get_object_or_404(ConnectionToken, pk=token_id) - if token.is_expired: - raise ValidationError({'id': 'Token is expired'}) - - token.is_valid() - serializer = self.get_serializer(instance=token) - expire_now = request.data.get('expire_now', True) - if expire_now: - token.expire() - - return Response(serializer.data, status=status.HTTP_200_OK) - def get_queryset(self): queryset = ConnectionToken.objects \ .filter(user=self.request.user) \ @@ -287,7 +245,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView permed_account = util.validate_permission(user, asset, account_name) if not permed_account or not permed_account.actions: - msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( + msg = 'user `{}` not has asset `{}` permission for account `{}`'.format( user, asset, account_name ) raise PermissionDenied(msg) @@ -305,10 +263,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView class SuperConnectionTokenViewSet(ConnectionTokenViewSet): serializer_classes = { 'default': SuperConnectionTokenSerializer, + 'get_secret_detail': ConnectionTokenSecretSerializer, } rbac_perms = { 'create': 'authentication.add_superconnectiontoken', - 'renewal': 'authentication.add_superconnectiontoken' + 'renewal': 'authentication.add_superconnectiontoken', + 'get_secret_detail': 'authentication.view_connectiontokensecret', + 'get_applet_info': 'authentication.view_superconnectiontoken', + 'release_applet_account': 'authentication.view_superconnectiontoken', } def get_queryset(self): @@ -332,3 +294,38 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): 'msg': f'Token is renewed, date expired: {date_expired}' } return Response(data=data, status=status.HTTP_200_OK) + + @action(methods=['POST'], detail=False, url_path='secret') + def get_secret_detail(self, request, *args, **kwargs): + """ 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """ + rbac_perm = 'authentication.view_connectiontokensecret' + if not request.user.has_perm(rbac_perm): + raise PermissionDenied('Not allow to view secret') + + token_id = request.data.get('id') or '' + token = get_object_or_404(ConnectionToken, pk=token_id) + if token.is_expired: + raise ValidationError({'id': 'Token is expired'}) + + token.is_valid() + serializer = self.get_serializer(instance=token) + expire_now = request.data.get('expire_now', True) + if expire_now: + token.expire() + return Response(serializer.data, status=status.HTTP_200_OK) + + @action(methods=['POST'], detail=False, url_path='applet-option') + def get_applet_info(self, *args, **kwargs): + token_id = self.request.data.get('id') + token = get_object_or_404(ConnectionToken, pk=token_id) + if token.is_expired: + return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST) + data = token.get_applet_option() + serializer = ConnectTokenAppletOptionSerializer(data) + return Response(serializer.data) + + @action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release') + def release_applet_account(self, *args, **kwargs): + account_id = self.request.data.get('id') + msg = ConnectionToken.release_applet_account(account_id) + return Response({'msg': msg}) diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index 70e0f759c..f29bf95e5 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -88,7 +88,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): """ @ssl_verification - def authenticate(self, request, nonce=None, **kwargs): + def authenticate(self, request, nonce=None, code_verifier=None, **kwargs): """ Authenticates users in case of the OpenID Connect Authorization code flow. """ log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}" logger.debug(log_prompt.format('start')) @@ -134,6 +134,8 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) ) } + if settings.AUTH_OPENID_PKCE and code_verifier: + token_payload['code_verifier'] = code_verifier if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post': token_payload.update({ 'client_id': settings.AUTH_OPENID_CLIENT_ID, diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index 78019ac33..88088245d 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -9,7 +9,10 @@ """ +import base64 +import hashlib import time +import secrets from django.conf import settings from django.contrib import auth @@ -38,6 +41,19 @@ class OIDCAuthRequestView(View): http_method_names = ['get', ] + @staticmethod + def gen_code_verifier(length=128): + # length range 43 ~ 128 + return secrets.token_urlsafe(length - 32) + + @staticmethod + def gen_code_challenge(code_verifier, code_challenge_method): + if code_challenge_method == 'plain': + return code_verifier + h = hashlib.sha256(code_verifier.encode('ascii')).digest() + b = base64.urlsafe_b64encode(h) + return b.decode('ascii')[:-1] + def get(self, request): """ Processes GET requests. """ @@ -56,6 +72,16 @@ class OIDCAuthRequestView(View): ) }) + if settings.AUTH_OPENID_PKCE: + code_verifier = self.gen_code_verifier() + code_challenge_method = settings.AUTH_OPENID_CODE_CHALLENGE_METHOD or 'S256' + code_challenge = self.gen_code_challenge(code_verifier, code_challenge_method) + authentication_request_params.update({ + 'code_challenge_method': code_challenge_method, + 'code_challenge': code_challenge + }) + request.session['oidc_auth_code_verifier'] = code_verifier + # States should be used! They are recommended in order to maintain state between the # authentication request and the callback. if settings.AUTH_OPENID_USE_STATE: @@ -138,8 +164,9 @@ class OIDCAuthCallbackView(View): # Authenticates the end-user. next_url = request.session.get('oidc_auth_next_url', None) + code_verifier = request.session.get('oidc_auth_code_verifier', None) logger.debug(log_prompt.format('Process authenticate')) - user = auth.authenticate(nonce=nonce, request=request) + user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier) if user and user.is_valid: logger.debug(log_prompt.format('Login: {}'.format(user))) auth.login(self.request, user) diff --git a/apps/authentication/migrations/0016_auto_20221220_1956.py b/apps/authentication/migrations/0016_auto_20221220_1956.py new file mode 100644 index 000000000..d1480629c --- /dev/null +++ b/apps/authentication/migrations/0016_auto_20221220_1956.py @@ -0,0 +1,58 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0015_auto_20221205_1136'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='ssotoken', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='temptoken', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='ssotoken', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='ssotoken', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='temptoken', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='temptoken', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 4a23d18b8..bb7d282b1 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -1,6 +1,9 @@ +import base64 +import json from datetime import timedelta from django.conf import settings +from django.core.cache import cache from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -8,17 +11,17 @@ from rest_framework.exceptions import PermissionDenied from assets.const import Protocol from common.db.fields import EncryptCharField -from common.db.models import JMSBaseModel -from common.utils import lazyproperty, pretty_string +from common.utils import lazyproperty, pretty_string, bulk_get from common.utils.timezone import as_current_tz -from orgs.mixins.models import OrgModelMixin +from orgs.mixins.models import JMSOrgBaseModel +from terminal.models import Applet def date_expired_default(): return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) -class ConnectionToken(OrgModelMixin, JMSBaseModel): +class ConnectionToken(JMSOrgBaseModel): value = models.CharField(max_length=64, default='', verbose_name=_("Value")) user = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True, blank=True, @@ -101,6 +104,9 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): error = _('No account') raise PermissionDenied(error) + if timezone.now() - self.date_created < timedelta(seconds=60): + return True, None + if not self.permed_account or not self.permed_account.actions: msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( self.user, self.asset, self.account @@ -115,6 +121,75 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): def platform(self): return self.asset.platform + @lazyproperty + def connect_method_object(self): + from common.utils import get_request_os + from jumpserver.utils import get_current_request + from terminal.connect_methods import ConnectMethodUtil + + request = get_current_request() + os = get_request_os(request) if request else 'windows' + method = ConnectMethodUtil.get_connect_method( + self.connect_method, protocol=self.protocol, os=os + ) + return method + + def get_remote_app_option(self): + cmdline = { + 'app_name': self.connect_method, + 'user_id': str(self.user.id), + 'asset_id': str(self.asset.id), + 'token_id': str(self.id) + } + cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode() + app = '||tinker' + options = { + 'remoteapplicationmode:i': '1', + 'remoteapplicationprogram:s': app, + 'remoteapplicationname:s': app, + 'alternate shell:s': app, + 'remoteapplicationcmdline:s': cmdline_b64, + } + return options + + def get_applet_option(self): + method = self.connect_method_object + if not method or method.get('type') != 'applet' or method.get('disabled', False): + return None + + applet = Applet.objects.filter(name=method.get('value')).first() + if not applet: + return None + + host_account = applet.select_host_account() + if not host_account: + return None + + host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl')) + gateway = host.gateway.select_gateway() if host.domain else None + + data = { + 'id': account.id, + 'applet': applet, + 'host': host, + 'gateway': gateway, + 'account': account, + 'remote_app_option': self.get_remote_app_option() + } + token_account_relate_key = f'token_account_relate_{account.id}' + cache.set(token_account_relate_key, lock_key, ttl) + return data + + @staticmethod + def release_applet_account(account_id): + token_account_relate_key = f'token_account_relate_{account_id}' + lock_key = cache.get(token_account_relate_key) + if lock_key: + cache.delete(lock_key) + cache.delete(token_account_relate_key) + return 'released' + return 'not found or expired' + @lazyproperty def account_object(self): from assets.models import Account diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py index f24f3e9c6..ac9345ae5 100644 --- a/apps/authentication/serializers/connect_token_secret.py +++ b/apps/authentication/serializers/connect_token_secret.py @@ -1,19 +1,19 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import ObjectRelatedField from acls.models import CommandGroup, CommandFilterACL +from assets.const import SecretType from assets.models import Asset, Account, Platform, Gateway, Domain from assets.serializers import PlatformSerializer, AssetProtocolsSerializer -from users.models import User -from perms.serializers.permission import ActionChoicesField +from common.drf.fields import LabeledChoiceField +from common.drf.fields import ObjectRelatedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin - +from perms.serializers.permission import ActionChoicesField +from users.models import User from ..models import ConnectionToken - __all__ = [ - 'ConnectionTokenSecretSerializer', + 'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer' ] @@ -36,6 +36,7 @@ class _ConnectionTokenAssetSerializer(serializers.ModelSerializer): class _SimpleAccountSerializer(serializers.ModelSerializer): """ Account """ + secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) class Meta: model = Account @@ -45,6 +46,7 @@ class _SimpleAccountSerializer(serializers.ModelSerializer): class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): """ Account """ su_from = _SimpleAccountSerializer(required=False, label=_('Su from')) + secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) class Meta: model = Account @@ -96,6 +98,24 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer): return names +class _ConnectionTokenConnectMethodSerializer(serializers.Serializer): + name = serializers.CharField(label=_('Name')) + protocol = serializers.CharField(label=_('Protocol')) + os = serializers.CharField(label=_('OS')) + is_builtin = serializers.BooleanField(label=_('Is builtin')) + is_active = serializers.BooleanField(label=_('Is active')) + platform = _ConnectionTokenPlatformSerializer(label=_('Platform')) + action = ActionChoicesField(label=_('Action')) + options = serializers.JSONField(label=_('Options')) + + +class _ConnectTokenConnectMethodSerializer(serializers.Serializer): + label = serializers.CharField(label=_('Label')) + value = serializers.CharField(label=_('Value')) + type = serializers.CharField(label=_('Type')) + component = serializers.CharField(label=_('Component')) + + class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): user = _ConnectionTokenUserSerializer(read_only=True) asset = _ConnectionTokenAssetSerializer(read_only=True) @@ -104,30 +124,28 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): platform = _ConnectionTokenPlatformSerializer(read_only=True) domain = ObjectRelatedField(queryset=Domain.objects, required=False, label=_('Domain')) command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True) + expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True) + connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object') actions = ActionChoicesField() expire_at = serializers.IntegerField() - expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True) - connect_method = serializers.SerializerMethodField(label=_('Connect method')) class Meta: model = ConnectionToken fields = [ 'id', 'value', 'user', 'asset', 'account', 'platform', 'command_filter_acls', 'protocol', - 'domain', 'gateway', 'actions', 'expire_at', 'expire_now', - 'connect_method' + 'domain', 'gateway', 'actions', 'expire_at', + 'expire_now', 'connect_method', ] extra_kwargs = { 'value': {'read_only': True}, } - def get_connect_method(self, obj): - from terminal.const import TerminalType - from common.utils import get_request_os - request = self.context.get('request') - if request: - os = get_request_os(request) - else: - os = 'windows' - method = TerminalType.get_connect_method(obj.connect_method, protocol=obj.protocol, os=os) - return method + +class ConnectTokenAppletOptionSerializer(serializers.Serializer): + id = serializers.CharField(label=_('ID')) + applet = ObjectRelatedField(read_only=True) + host = _ConnectionTokenAssetSerializer(read_only=True) + account = _ConnectionTokenAccountSerializer(read_only=True) + gateway = _ConnectionTokenGatewaySerializer(read_only=True) + remote_app_option = serializers.JSONField(read_only=True) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 2b5b156e8..2fd9ae16e 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from common.drf.fields import EncryptedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin - from ..models import ConnectionToken __all__ = [ @@ -12,6 +12,9 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) + input_secret = EncryptedField( + label=_("Input secret"), max_length=40960, required=False, allow_blank=True + ) class Meta: model = ConnectionToken diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 2cc0d2781..28d790597 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -18,6 +18,23 @@ @@ -215,11 +248,31 @@
-
-
- +
+
+

+ {% trans 'Login' %} +

+
-
+
{% csrf_token %}
@@ -227,37 +280,17 @@

{{ form.non_field_errors.as_text }}

- {% else %} -

- {% trans 'Welcome back, please enter username and password to login' %} -

{% endif %}
- - {% bootstrap_field form.username show_label=False %}
- - + + {% if form.password.errors %} -

+

{{ form.password.errors.as_text }}

{% endif %} @@ -274,7 +307,7 @@
{% endif %} If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck.
" +msgstr "" +"
Lunaは個別にデプロイされたプログラムです。Luna、kokoをデプロイする必要" +"があります。urlディストリビューションにnginxを設定します。
この" +"ページが表示されている場合は、nginxリスニングポートにアクセスしていないことを" +"証明してください。頑張ってください。
" + +#: jumpserver/views/other.py:70 +msgid "Websocket server run on port: {}, you should proxy it on nginx" +msgstr "" +"Websocket サーバーはport: {}で実行されます。nginxでプロキシする必要がありま" +"す。" + +#: jumpserver/views/other.py:84 +msgid "" +"
Koko is a separately deployed program, you need to deploy Koko, " +"configure nginx for url distribution,
If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck.
" +msgstr "" +"
Kokoは個別にデプロイされているプログラムです。Kokoをデプロイする必要が" +"あります。URL配布用にnginxを設定します。
このページが表示されて" +"いる場合は、nginxリスニングポートにアクセスしていないことを証明してください。" +"頑張ってください。" + +#: notifications/apps.py:7 +msgid "Notifications" +msgstr "通知" + +#: notifications/backends/__init__.py:13 +msgid "Site message" +msgstr "サイトメッセージ" + +#: notifications/models/notification.py:14 +msgid "receive backend" +msgstr "メッセージのバックエンド" + +#: notifications/models/notification.py:18 +msgid "User message" +msgstr "ユーザメッセージ" + +#: notifications/models/notification.py:21 +msgid "{} subscription" +msgstr "{} 購読" + +#: notifications/models/notification.py:34 +msgid "System message" +msgstr "システムメッセージ" + +#: notifications/notifications.py:46 +msgid "Publish the station message" +msgstr "" + +#: ops/ansible/inventory.py:76 +#, fuzzy +msgid "No account available" +msgstr "利用できないアカウント" + +#: ops/ansible/inventory.py:180 +#, fuzzy +msgid "Ansible disabled" +msgstr "ユーザーが無効になりました。" + +#: ops/ansible/inventory.py:196 +msgid "Skip hosts below:" +msgstr "" + +#: ops/api/celery.py:63 ops/api/celery.py:78 +msgid "Waiting task start" +msgstr "タスク開始待ち" + +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +msgid "App ops" +msgstr "アプリ操作" + +#: ops/const.py:6 +msgid "Push" +msgstr "" + +#: ops/const.py:7 +#, fuzzy +msgid "Verify" +msgstr "確認済み" + +#: ops/const.py:8 +msgid "Collect" +msgstr "" + +#: ops/const.py:9 +#, fuzzy +msgid "Change password" +msgstr "パスワードの変更" + +#: ops/const.py:19 +msgid "Custom password" +msgstr "カスタムパスワード" + +#: ops/const.py:33 +msgid "Adhoc" +msgstr "コマンド#コマンド#" + +#: ops/const.py:34 ops/models/job.py:31 +msgid "Playbook" +msgstr "Playbook" + +#: ops/const.py:38 +msgid "Privileged Only" +msgstr "特権アカウントのみ" + +#: ops/const.py:39 +msgid "Privileged First" +msgstr "特権アカウント優先" + +#: ops/const.py:40 +msgid "Skip" +msgstr "スキップ" + +#: ops/const.py:45 ops/models/adhoc.py:20 +#, fuzzy +#| msgid "PowerShell" +msgid "Powershell" +msgstr "PowerShell" + +#: ops/exception.py:6 +msgid "no valid program entry found." +msgstr "利用可能なプログラムポータルがありません" + +#: ops/mixin.py:25 ops/mixin.py:88 settings/serializers/auth/ldap.py:73 +msgid "Cycle perform" +msgstr "サイクル実行" + +#: ops/mixin.py:29 ops/mixin.py:86 ops/mixin.py:105 +#: settings/serializers/auth/ldap.py:70 +msgid "Regularly perform" +msgstr "定期的に実行する" + +#: ops/mixin.py:108 +msgid "Interval" +msgstr "間隔" + +#: ops/mixin.py:118 +msgid "* Please enter a valid crontab expression" +msgstr "* 有効なcrontab式を入力してください" + +#: ops/mixin.py:125 +msgid "Range {} to {}" +msgstr "{} から {} までの範囲" + +#: ops/mixin.py:136 +msgid "Require periodic or regularly perform setting" +msgstr "定期的または定期的に設定を行う必要があります" + +#: ops/models/adhoc.py:24 +msgid "Pattern" +msgstr "パターン" + +#: ops/models/adhoc.py:26 ops/models/job.py:28 +msgid "Module" +msgstr "モジュール" + +#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: terminal/models/component/task.py:16 +msgid "Args" +msgstr "アルグ" + +#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: terminal/models/session/sharing.py:23 +msgid "Creator" +msgstr "作成者" + +#: ops/models/adhoc.py:46 +msgid "AdHoc" +msgstr "タスクの各バージョン" + +#: ops/models/base.py:19 +#, fuzzy +msgid "Account policy" +msgstr "アカウントキー" + +#: ops/models/base.py:20 +#, fuzzy +msgid "Last execution" +msgstr "コマンド実行" + +#: ops/models/base.py:22 +#, fuzzy +msgid "Date last run" +msgstr "最終同期日" + +#: ops/models/base.py:51 ops/models/job.py:102 +#: xpack/plugins/cloud/models.py:170 +msgid "Result" +msgstr "結果" + +#: ops/models/base.py:52 ops/models/job.py:103 +msgid "Summary" +msgstr "概要" + +#: ops/models/celery.py:47 +msgid "Celery Task" +msgstr "Celery タスク#タスク#" + +#: ops/models/celery.py:56 terminal/models/component/task.py:17 +msgid "Kwargs" +msgstr "クワーグ" + +#: ops/models/celery.py:57 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: tickets/serializers/ticket/ticket.py:21 +msgid "State" +msgstr "状態" + +#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: tickets/const.py:25 +msgid "Finished" +msgstr "終了" + +#: ops/models/celery.py:59 +#, fuzzy +msgid "Date published" +msgstr "終了日" + +#: ops/models/celery.py:83 +msgid "Celery Task Execution" +msgstr "Celery タスク実行" + +#: ops/models/job.py:29 +msgid "Chdir" +msgstr "Chdir" + +#: ops/models/job.py:30 +msgid "Timeout (Seconds)" +msgstr "タイムアウト(秒)" + +#: ops/models/job.py:35 +msgid "Runas" +msgstr "Runas" + +#: ops/models/job.py:37 +msgid "Runas policy" +msgstr "Runas ポリシー" + +#: ops/models/job.py:38 +msgid "Use Parameter Define" +msgstr "パラメータ定義を使用する" + +#: ops/models/job.py:39 +msgid "Parameters define" +msgstr "パラメータ定義" + +#: ops/models/job.py:91 +msgid "Job" +msgstr "ジョブ#ジョブ#" + +#: ops/models/job.py:101 +msgid "Parameters" +msgstr "パラメータ" + +#: ops/models/job.py:300 +msgid "Job Execution" +msgstr "ジョブ実行" + +#: ops/models/job.py:311 +msgid "Job audit log" +msgstr "ジョブ監査ログ" + +#: ops/notifications.py:17 +msgid "Server performance" +msgstr "サーバーのパフォーマンス" + +#: ops/notifications.py:23 +msgid "Terminal health check warning" +msgstr "ターミナルヘルスチェックの警告" + +#: ops/notifications.py:68 +#, python-brace-format +msgid "The terminal is offline: {name}" +msgstr "ターミナルはオフラインです: {name}" + +#: ops/notifications.py:73 +#, python-brace-format +msgid "Disk used more than {max_threshold}%: => {value}" +msgstr "{max_threshold}%: => {value} を超えるディスクを使用" + +#: ops/notifications.py:78 +#, python-brace-format +msgid "Memory used more than {max_threshold}%: => {value}" +msgstr "{max_threshold}%: => {value} を超える使用メモリ" + +#: ops/notifications.py:83 +#, python-brace-format +msgid "CPU load more than {max_threshold}: => {value}" +msgstr "{max_threshold} を超えるCPUロード: => {value}" + +#: ops/serializers/job.py:14 +#, fuzzy +msgid "Run after save" +msgstr "システムユーザーの実行" + +#: ops/serializers/job.py:43 +#, fuzzy +msgid "Job type" +msgstr "Docタイプ" + +#: ops/serializers/job.py:44 +msgid "Material" +msgstr "マテリアル" + +#: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 +#: terminal/models/component/task.py:24 +msgid "Task" +msgstr "タスク" + +#: ops/tasks.py:28 +#, fuzzy +msgid "Run ansible task" +msgstr "アセットの実行" + +#: ops/tasks.py:35 +#, fuzzy +msgid "Run ansible task execution" +msgstr "インスタンスタスクの同期実行" + +#: ops/tasks.py:49 +msgid "Periodic clear celery tasks" +msgstr "定期的にCeleryタスクをクリア" + +#: ops/tasks.py:51 +msgid "Clean celery log period" +msgstr "きれいなセロリログ期間" + +#: ops/tasks.py:68 +#, fuzzy +msgid "Clear celery periodic tasks" +msgstr "きれいなセロリログ期間" + +#: ops/tasks.py:91 +msgid "Create or update periodic tasks" +msgstr "定期的なタスクの作成または更新" + +#: ops/tasks.py:99 +#, fuzzy +msgid "Periodic check service performance" +msgstr "定期的なパフォーマンス" + +#: ops/templates/ops/celery_task_log.html:4 +msgid "Task log" +msgstr "タスクログ" + +#: ops/utils.py:64 +msgid "Update task content: {}" +msgstr "タスク内容の更新: {}" + +#: ops/variables.py:24 +msgid "The current user`s username of JumpServer" +msgstr "JumpServerの現在のユーザーのユーザー名" + +#: ops/variables.py:25 +msgid "The id of the asset in the JumpServer" +msgstr "JumpServerのアセットのID" + +#: ops/variables.py:26 +msgid "The type of the asset in the JumpServer" +msgstr "JumpServerのアセットのタイプ" + +#: ops/variables.py:27 +msgid "The category of the asset in the JumpServer" +msgstr "JumpServerのアセットのカテゴリ" + +#: ops/variables.py:28 +msgid "The name of the asset in the JumpServer" +msgstr "JumpServerのアセットの名前" + +#: ops/variables.py:29 +msgid "Address used to connect this asset in JumpServer" +msgstr "JumpServerでこのアセットに接続するために使用されるアドレス" + +#: ops/variables.py:30 +msgid "Port used to connect this asset in JumpServer" +msgstr "JumpServerでこのアセットに接続するために使用されるポート" + +#: ops/variables.py:31 +msgid "ID of the job" +msgstr "ジョブのID" + +#: ops/variables.py:32 +msgid "Name of the job" +msgstr "ジョブの名前" + +#: orgs/api.py:67 +msgid "The current organization ({}) cannot be deleted" +msgstr "現在の組織 ({}) は削除できません" + +#: orgs/api.py:72 +msgid "" +"LDAP synchronization is set to the current organization. Please switch to " +"another organization before deleting" +msgstr "" +"LDAP 同期は現在の組織に設定されます。削除する前に別の組織に切り替えてください" + +#: orgs/api.py:81 +msgid "The organization have resource ({}) cannot be deleted" +msgstr "組織のリソース ({}) は削除できません" + +#: orgs/apps.py:7 rbac/tree.py:119 +msgid "App organizations" +msgstr "アプリ組織" + +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: rbac/const.py:7 rbac/models/rolebinding.py:48 +#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +msgid "Organization" +msgstr "組織" + +#: orgs/mixins/serializers.py:26 rbac/serializers/rolebinding.py:23 +msgid "Org name" +msgstr "組織名" + +#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +#, fuzzy +msgid "Builtin" +msgstr "内蔵" + +#: orgs/models.py:74 +msgid "GLOBAL" +msgstr "グローバル組織" + +#: orgs/models.py:76 +msgid "DEFAULT" +msgstr "デフォルト組織" + +#: orgs/models.py:78 +msgid "SYSTEM" +msgstr "システム組織" + +#: orgs/models.py:84 +msgid "Can view root org" +msgstr "グローバル組織を表示できます" + +#: orgs/models.py:85 +msgid "Can view all joined org" +msgstr "参加しているすべての組織を表示できます" + +#: orgs/tasks.py:9 +#, fuzzy +msgid "Refresh organization cache" +msgstr "グローバル組織名" + +#: perms/apps.py:9 +msgid "App permissions" +msgstr "アプリの権限" + +#: perms/const.py:12 +msgid "Connect" +msgstr "接続" + +#: perms/const.py:15 +#, fuzzy +msgid "Copy" +msgstr "リンクのコピー" + +#: perms/const.py:16 +msgid "Paste" +msgstr "貼り付け" + +#: perms/const.py:26 +msgid "Transfer" +msgstr "転送" + +#: perms/const.py:27 +#, fuzzy +msgid "Clipboard" +msgstr "クリップボードのコピー" + +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:59 +#: tickets/models/ticket/apply_application.py:28 +#: tickets/models/ticket/apply_asset.py:18 +msgid "Actions" +msgstr "アクション" + +#: perms/models/asset_permission.py:76 +msgid "From ticket" +msgstr "チケットから" + +#: perms/models/asset_permission.py:82 +msgid "Asset permission" +msgstr "資産権限" + +#: perms/models/perm_node.py:67 +msgid "Ungrouped" +msgstr "グループ化されていません" + +#: perms/models/perm_node.py:69 +msgid "Favorite" +msgstr "お気に入り" + +#: perms/models/perm_node.py:120 +msgid "Permed asset" +msgstr "許可された資産" + +#: perms/models/perm_node.py:122 +msgid "Can view my assets" +msgstr "私の資産を見ることができます" + +#: perms/models/perm_node.py:123 +msgid "Can view user assets" +msgstr "ユーザー資産を表示できます" + +#: perms/models/perm_node.py:124 +msgid "Can view usergroup assets" +msgstr "ユーザーグループの資産を表示できます" + +#: perms/models/perm_node.py:135 +#, fuzzy +msgid "Permed account" +msgstr "アカウントを集める" + +#: perms/notifications.py:12 perms/notifications.py:44 +msgid "today" +msgstr "今" + +#: perms/notifications.py:15 +msgid "You permed assets is about to expire" +msgstr "パーマ資産の有効期限が近づいています" + +#: perms/notifications.py:20 +msgid "permed assets" +msgstr "パーマ資産" + +#: perms/notifications.py:59 +msgid "Asset permissions is about to expire" +msgstr "資産権限の有効期限が近づいています" + +#: perms/notifications.py:64 +msgid "asset permissions of organization {}" +msgstr "組織 {} の資産権限" + +#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: users/serializers/user.py:91 users/serializers/user.py:161 +msgid "Is expired" +msgstr "期限切れです" + +#: perms/templates/perms/_msg_item_permissions_expire.html:7 +#: perms/templates/perms/_msg_permed_items_expire.html:7 +#, python-format +msgid "" +"\n" +" The following %(item_type)s will expire in %(count)s days\n" +" " +msgstr "" +"\n" +" 次の %(item_type)s は %(count)s 日以内に期限切れになります\n" +" " + +#: perms/templates/perms/_msg_permed_items_expire.html:21 +msgid "If you have any question, please contact the administrator" +msgstr "質問があったら、管理者に連絡して下さい" + +#: rbac/api/role.py:34 +msgid "Internal role, can't be destroy" +msgstr "内部の役割は、破壊することはできません" + +#: rbac/api/role.py:38 +msgid "The role has been bound to users, can't be destroy" +msgstr "ロールはユーザーにバインドされており、破壊することはできません" + +#: rbac/api/role.py:60 +msgid "Internal role, can't be update" +msgstr "内部ロール、更新できません" + +#: rbac/api/rolebinding.py:52 +msgid "{} at least one system role" +msgstr "{} 少なくとも1つのシステムロール" + +#: rbac/apps.py:7 +msgid "RBAC" +msgstr "RBAC" + +#: rbac/builtin.py:111 +msgid "SystemAdmin" +msgstr "システム管理者" + +#: rbac/builtin.py:114 +msgid "SystemAuditor" +msgstr "システム監査人" + +#: rbac/builtin.py:117 +msgid "SystemComponent" +msgstr "システムコンポーネント" + +#: rbac/builtin.py:123 +msgid "OrgAdmin" +msgstr "組織管理者" + +#: rbac/builtin.py:126 +msgid "OrgAuditor" +msgstr "監査員を組織する" + +#: rbac/builtin.py:129 +msgid "OrgUser" +msgstr "組織ユーザー" + +#: rbac/models/menu.py:13 +msgid "Menu permission" +msgstr "メニュー権限" + +#: rbac/models/menu.py:15 +msgid "Can view console view" +msgstr "コンソールビューを表示できます" + +#: rbac/models/menu.py:16 +msgid "Can view audit view" +msgstr "監査ビューを表示できます" + +#: rbac/models/menu.py:17 +msgid "Can view workbench view" +msgstr "ワークスペースビューを表示できます" + +#: rbac/models/menu.py:18 +msgid "Can view web terminal" +msgstr "Webターミナルを表示できます" + +#: rbac/models/menu.py:19 +msgid "Can view file manager" +msgstr "ファイルマネージャを表示できます" + +#: rbac/models/permission.py:26 rbac/models/role.py:34 +msgid "Permissions" +msgstr "権限" + +#: rbac/models/role.py:31 rbac/models/rolebinding.py:38 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +msgid "Scope" +msgstr "スコープ" + +#: rbac/models/role.py:46 rbac/models/rolebinding.py:44 +#: users/models/user.py:685 +msgid "Role" +msgstr "ロール" + +#: rbac/models/role.py:144 +msgid "System role" +msgstr "システムの役割" + +#: rbac/models/role.py:152 +msgid "Organization role" +msgstr "組織の役割" + +#: rbac/models/rolebinding.py:53 +msgid "Role binding" +msgstr "ロールバインディング" + +#: rbac/models/rolebinding.py:137 +msgid "All organizations" +msgstr "全ての組織" + +#: rbac/models/rolebinding.py:166 +msgid "" +"User last role in org, can not be delete, you can remove user from org " +"instead" +msgstr "" +"ユーザーの最後のロールは削除できません。ユーザーを組織から削除できます。" + +#: rbac/models/rolebinding.py:173 +msgid "Organization role binding" +msgstr "組織の役割バインディング" + +#: rbac/models/rolebinding.py:188 +msgid "System role binding" +msgstr "システムロールバインディング" + +#: rbac/serializers/permission.py:26 users/serializers/profile.py:132 +msgid "Perms" +msgstr "パーマ" + +#: rbac/serializers/role.py:27 users/serializers/group.py:34 +msgid "Users amount" +msgstr "ユーザー数" + +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +msgid "Display name" +msgstr "表示名" + +#: rbac/serializers/rolebinding.py:22 +msgid "Role display" +msgstr "ロール表示" + +#: rbac/serializers/rolebinding.py:56 +msgid "Has bound this role" +msgstr "この役割をバインドしました" + +#: rbac/tree.py:18 rbac/tree.py:19 +msgid "All permissions" +msgstr "すべての権限" + +#: rbac/tree.py:25 +msgid "Console view" +msgstr "コンソールビュー" + +#: rbac/tree.py:26 +msgid "Workbench view" +msgstr "ワークスペースビュー" + +#: rbac/tree.py:27 +msgid "Audit view" +msgstr "監査ビュー" + +#: rbac/tree.py:28 settings/models.py:159 +msgid "System setting" +msgstr "システム設定" + +#: rbac/tree.py:29 +msgid "Other" +msgstr "その他" + +#: rbac/tree.py:38 +msgid "Session audits" +msgstr "セッション監査" + +#: rbac/tree.py:48 +msgid "Cloud import" +msgstr "クラウドインポート" + +#: rbac/tree.py:49 +msgid "Backup account" +msgstr "バックアップアカウント" + +#: rbac/tree.py:50 +msgid "Gather account" +msgstr "アカウントを集める" + +#: rbac/tree.py:51 +msgid "Asset change auth" +msgstr "資産の改ざん" + +#: rbac/tree.py:52 +msgid "Terminal setting" +msgstr "ターミナル設定" + +#: rbac/tree.py:53 +msgid "Task Center" +msgstr "タスクセンター" + +#: rbac/tree.py:54 +msgid "My assets" +msgstr "私の資産" + +#: rbac/tree.py:56 terminal/models/applet/applet.py:38 +#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +msgid "Applet" +msgstr "リモートアプリケーション" + +#: rbac/tree.py:120 +msgid "Ticket comment" +msgstr "チケットコメント" + +#: rbac/tree.py:121 tickets/models/ticket/general.py:307 +msgid "Ticket" +msgstr "チケット" + +#: rbac/tree.py:122 +msgid "Common setting" +msgstr "共通設定" + +#: rbac/tree.py:123 +msgid "View permission tree" +msgstr "権限ツリーの表示" + +#: rbac/tree.py:124 +msgid "Execute batch command" +msgstr "バッチ実行コマンド" + +#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 +#: settings/api/sms.py:148 settings/api/wecom.py:37 +msgid "Test success" +msgstr "テストの成功" + +#: settings/api/email.py:20 +msgid "Test mail sent to {}, please check" +msgstr "{}に送信されたテストメールを確認してください" + +#: settings/api/ldap.py:166 +msgid "Synchronization start, please wait." +msgstr "同期開始、お待ちください。" + +#: settings/api/ldap.py:170 +msgid "Synchronization is running, please wait." +msgstr "同期が実行中です。しばらくお待ちください。" + +#: settings/api/ldap.py:175 +msgid "Synchronization error: {}" +msgstr "同期エラー: {}" + +#: settings/api/ldap.py:213 +msgid "Get ldap users is None" +msgstr "Ldapユーザーを取得するにはNone" + +#: settings/api/ldap.py:222 +msgid "Imported {} users successfully (Organization: {})" +msgstr "{} 人のユーザーを正常にインポートしました (組織: {})" + +#: settings/api/sms.py:130 +msgid "Invalid SMS platform" +msgstr "無効なショートメッセージプラットフォーム" + +#: settings/api/sms.py:136 +msgid "test_phone is required" +msgstr "携帯番号をテストこのフィールドは必須です" + +#: settings/apps.py:7 +msgid "Settings" +msgstr "設定" + +#: settings/models.py:36 +msgid "Encrypted" +msgstr "暗号化された" + +#: settings/models.py:161 +msgid "Can change email setting" +msgstr "メール設定を変更できます" + +#: settings/models.py:162 +msgid "Can change auth setting" +msgstr "資格認定の設定" + +#: settings/models.py:163 +msgid "Can change system msg sub setting" +msgstr "システムmsgサブ设定を変更できます" + +#: settings/models.py:164 +msgid "Can change sms setting" +msgstr "Smsの設定を変えることができます" + +#: settings/models.py:165 +msgid "Can change security setting" +msgstr "セキュリティ設定を変更できます" + +#: settings/models.py:166 +msgid "Can change clean setting" +msgstr "きれいな設定を変えることができます" + +#: settings/models.py:167 +msgid "Can change interface setting" +msgstr "インターフェイスの設定を変えることができます" + +#: settings/models.py:168 +msgid "Can change license setting" +msgstr "ライセンス設定を変更できます" + +#: settings/models.py:169 +msgid "Can change terminal setting" +msgstr "ターミナルの設定を変えることができます" + +#: settings/models.py:170 +msgid "Can change other setting" +msgstr "他の設定を変えることができます" + +#: settings/serializers/auth/base.py:12 +msgid "CAS Auth" +msgstr "CAS 認証" + +#: settings/serializers/auth/base.py:13 +msgid "OPENID Auth" +msgstr "OPENID 認証" + +#: settings/serializers/auth/base.py:14 +msgid "RADIUS Auth" +msgstr "RADIUS 認証" + +#: settings/serializers/auth/base.py:15 +msgid "DingTalk Auth" +msgstr "くぎ 認証" + +#: settings/serializers/auth/base.py:16 +msgid "FeiShu Auth" +msgstr "飛本 認証" + +#: settings/serializers/auth/base.py:17 +msgid "WeCom Auth" +msgstr "企業微信 認証" + +#: settings/serializers/auth/base.py:18 +msgid "SSO Auth" +msgstr "SSO Token 認証" + +#: settings/serializers/auth/base.py:19 +msgid "SAML2 Auth" +msgstr "SAML2 認証" + +#: settings/serializers/auth/base.py:22 settings/serializers/basic.py:38 +msgid "Forgot password url" +msgstr "パスワードのURLを忘れた" + +#: settings/serializers/auth/base.py:28 +msgid "Enable login redirect msg" +msgstr "ログインリダイレクトの有効化msg" + +#: settings/serializers/auth/cas.py:10 +msgid "CAS" +msgstr "CAS" + +#: settings/serializers/auth/cas.py:12 +msgid "Enable CAS Auth" +msgstr "CAS 認証の有効化" + +#: settings/serializers/auth/cas.py:13 settings/serializers/auth/oidc.py:54 +msgid "Server url" +msgstr "サービス側アドレス" + +#: settings/serializers/auth/cas.py:16 +msgid "Proxy server url" +msgstr "コールバックアドレス" + +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/saml2.py:34 +msgid "Logout completely" +msgstr "同期ログアウト" + +#: settings/serializers/auth/cas.py:23 +msgid "Username attr" +msgstr "ユーザー名のプロパティ" + +#: settings/serializers/auth/cas.py:26 +msgid "Enable attributes map" +msgstr "属性マップの有効化" + +#: settings/serializers/auth/cas.py:28 settings/serializers/auth/saml2.py:33 +msgid "Rename attr" +msgstr "マッピングのプロパティ" + +#: settings/serializers/auth/cas.py:29 +msgid "Create user if not" +msgstr "そうでない場合はユーザーを作成" + +#: settings/serializers/auth/dingtalk.py:15 +msgid "Enable DingTalk Auth" +msgstr "ピン認証の有効化" + +#: settings/serializers/auth/feishu.py:14 +msgid "Enable FeiShu Auth" +msgstr "飛本認証の有効化" + +#: settings/serializers/auth/ldap.py:39 +msgid "LDAP" +msgstr "LDAP" + +#: settings/serializers/auth/ldap.py:42 +msgid "LDAP server" +msgstr "LDAPサーバー" + +#: settings/serializers/auth/ldap.py:43 +msgid "eg: ldap://localhost:389" +msgstr "例: ldap://localhost:389" + +#: settings/serializers/auth/ldap.py:45 +msgid "Bind DN" +msgstr "DN のバインド" + +#: settings/serializers/auth/ldap.py:50 +msgid "User OU" +msgstr "ユーザー OU" + +#: settings/serializers/auth/ldap.py:51 +msgid "Use | split multi OUs" +msgstr "使用 | splitマルチ OU" + +#: settings/serializers/auth/ldap.py:54 +msgid "User search filter" +msgstr "ユーザー検索フィルター" + +#: settings/serializers/auth/ldap.py:55 +#, python-format +msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" +msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)" + +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/oidc.py:37 +msgid "User attr map" +msgstr "ユーザー属性マッピング" + +#: settings/serializers/auth/ldap.py:59 +msgid "" +"User attr map present how to map LDAP user attr to jumpserver, username,name," +"email is jumpserver attr" +msgstr "" +"ユーザー属性マッピングは、LDAPのユーザー属性をjumpserverユーザーにマッピング" +"する方法、username, name,emailはjumpserverのユーザーが必要とする属性です" + +#: settings/serializers/auth/ldap.py:77 +msgid "Connect timeout" +msgstr "接続タイムアウト" + +#: settings/serializers/auth/ldap.py:79 +msgid "Search paged size" +msgstr "ページサイズを検索" + +#: settings/serializers/auth/ldap.py:81 +msgid "Enable LDAP auth" +msgstr "LDAP認証の有効化" + +#: settings/serializers/auth/oauth2.py:19 +msgid "OAuth2" +msgstr "OAuth2" + +#: settings/serializers/auth/oauth2.py:22 +msgid "Enable OAuth2 Auth" +msgstr "OAuth2認証の有効化" + +#: settings/serializers/auth/oauth2.py:25 +msgid "Logo" +msgstr "アイコン" + +#: settings/serializers/auth/oauth2.py:28 +msgid "Service provider" +msgstr "サービスプロバイダー" + +#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +msgid "Client Id" +msgstr "クライアントID" + +#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: xpack/plugins/cloud/serializers/account_attrs.py:38 +msgid "Client Secret" +msgstr "クライアント秘密" + +#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +msgid "Provider auth endpoint" +msgstr "認証エンドポイントアドレス" + +#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +msgid "Provider token endpoint" +msgstr "プロバイダートークンエンドポイント" + +#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +msgid "Client authentication method" +msgstr "クライアント認証方式" + +#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +msgid "Provider userinfo endpoint" +msgstr "プロバイダーuserinfoエンドポイント" + +#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +msgid "Provider end session endpoint" +msgstr "プロバイダーのセッション終了エンドポイント" + +#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/saml2.py:35 +msgid "Always update user" +msgstr "常にユーザーを更新" + +#: settings/serializers/auth/oidc.py:12 +msgid "OIDC" +msgstr "OIDC" + +#: settings/serializers/auth/oidc.py:16 +msgid "Base site url" +msgstr "ベースサイトのアドレス" + +#: settings/serializers/auth/oidc.py:32 +msgid "Share session" +msgstr "セッションの共有" + +#: settings/serializers/auth/oidc.py:34 +msgid "Ignore ssl verification" +msgstr "Ssl検証を無視する" + +#: settings/serializers/auth/oidc.py:38 +msgid "" +"User attr map present how to map OpenID user attr to jumpserver, username," +"name,email is jumpserver attr" +msgstr "" +"ユーザー属性マッピングは、OpenIDのユーザー属性をjumpserverユーザーにマッピン" +"グする方法、username, name,emailはjumpserverのユーザーが必要とする属性です" + +#: settings/serializers/auth/oidc.py:41 +#, fuzzy +#| msgid "Enable OTP" +msgid "Enable PKCE" +msgstr "OTPの有効化" + +#: settings/serializers/auth/oidc.py:43 +#, fuzzy +msgid "Code challenge method" +msgstr "接続タイムアウト" + +#: settings/serializers/auth/oidc.py:51 +msgid "Use Keycloak" +msgstr "Keycloakを使用する" + +#: settings/serializers/auth/oidc.py:57 +msgid "Realm name" +msgstr "レルム名" + +#: settings/serializers/auth/oidc.py:63 +msgid "Enable OPENID Auth" +msgstr "OIDC認証の有効化" + +#: settings/serializers/auth/oidc.py:65 +msgid "Provider endpoint" +msgstr "プロバイダーエンドポイント" + +#: settings/serializers/auth/oidc.py:74 +msgid "Provider jwks endpoint" +msgstr "プロバイダーjwksエンドポイント" + +#: settings/serializers/auth/oidc.py:83 +msgid "Provider sign alg" +msgstr "プロビダーサインalg" + +#: settings/serializers/auth/oidc.py:86 +msgid "Provider sign key" +msgstr "プロバイダ署名キー" + +#: settings/serializers/auth/oidc.py:88 +msgid "Scopes" +msgstr "スコープ" + +#: settings/serializers/auth/oidc.py:90 +msgid "Id token max age" +msgstr "IDトークンの最大年齢" + +#: settings/serializers/auth/oidc.py:93 +msgid "Id token include claims" +msgstr "IDトークンにはクレームが含まれます" + +#: settings/serializers/auth/oidc.py:95 +msgid "Use state" +msgstr "使用状態" + +#: settings/serializers/auth/oidc.py:96 +msgid "Use nonce" +msgstr "Nonceを使用" + +#: settings/serializers/auth/radius.py:13 +msgid "Radius" +msgstr "Radius" + +#: settings/serializers/auth/radius.py:15 +msgid "Enable Radius Auth" +msgstr "Radius認証の有効化" + +#: settings/serializers/auth/radius.py:21 +msgid "OTP in Radius" +msgstr "Radius のOTP" + +#: settings/serializers/auth/saml2.py:11 +msgid "SAML2" +msgstr "SAML2" + +#: settings/serializers/auth/saml2.py:14 +msgid "Enable SAML2 Auth" +msgstr "SAML2認証の有効化" + +#: settings/serializers/auth/saml2.py:17 +msgid "IDP metadata URL" +msgstr "IDP metadata アドレス" + +#: settings/serializers/auth/saml2.py:20 +msgid "IDP metadata XML" +msgstr "IDP metadata XML" + +#: settings/serializers/auth/saml2.py:23 +msgid "SP advanced settings" +msgstr "詳細設定" + +#: settings/serializers/auth/saml2.py:27 +msgid "SP private key" +msgstr "SP プライベートキー" + +#: settings/serializers/auth/saml2.py:31 +msgid "SP cert" +msgstr "SP 証明書" + +#: settings/serializers/auth/sms.py:15 +msgid "Enable SMS" +msgstr "SMSの有効化" + +#: settings/serializers/auth/sms.py:17 +msgid "SMS provider / Protocol" +msgstr "SMSプロバイダ / プロトコル" + +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +msgid "Signature" +msgstr "署名" + +#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:46 +#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 +msgid "Template code" +msgstr "テンプレートコード" + +#: settings/serializers/auth/sms.py:31 +msgid "Test phone" +msgstr "テスト電話" + +#: settings/serializers/auth/sms.py:60 +msgid "App Access Address" +msgstr "アプリケーションアドレス" + +#: settings/serializers/auth/sms.py:61 +msgid "Signature channel number" +msgstr "署名チャネル番号" + +#: settings/serializers/auth/sms.py:69 +msgid "Enterprise code(SP id)" +msgstr "企業コード(SP id)" + +#: settings/serializers/auth/sms.py:70 +msgid "Shared secret(Shared secret)" +msgstr "パスワードを共有する(Shared secret)" + +#: settings/serializers/auth/sms.py:71 +msgid "Original number(Src id)" +msgstr "元の番号(Src id)" + +#: settings/serializers/auth/sms.py:72 +msgid "Business type(Service id)" +msgstr "ビジネス・タイプ(Service id)" + +#: settings/serializers/auth/sms.py:75 +msgid "Template" +msgstr "テンプレート" + +#: settings/serializers/auth/sms.py:76 +#, python-brace-format +msgid "" +"Template need contain {code} and Signature + template length does not exceed " +"67 words. For example, your verification code is {code}, which is valid for " +"5 minutes. Please do not disclose it to others." +msgstr "" +"テンプレートには{code}を含める必要があり、署名+テンプレートの長さは67ワード未" +"満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない" +"でください。" + +#: settings/serializers/auth/sms.py:85 +#, python-brace-format +msgid "The template needs to contain {code}" +msgstr "テンプレートには{code}を含める必要があります" + +#: settings/serializers/auth/sms.py:88 +msgid "Signature + Template must not exceed 65 words" +msgstr "署名+テンプレートの長さは65文字以内" + +#: settings/serializers/auth/sso.py:13 +msgid "Enable SSO auth" +msgstr "SSO Token認証の有効化" + +#: settings/serializers/auth/sso.py:14 +msgid "Other service can using SSO token login to JumpServer without password" +msgstr "" +"他のサービスはパスワードなしでJumpServerへのSSOトークンログインを使用できます" + +#: settings/serializers/auth/sso.py:17 +msgid "SSO auth key TTL" +msgstr "Token有効期間" + +#: settings/serializers/auth/sso.py:17 +#: xpack/plugins/cloud/serializers/account_attrs.py:176 +msgid "Unit: second" +msgstr "単位: 秒" + +#: settings/serializers/auth/wecom.py:15 +msgid "Enable WeCom Auth" +msgstr "企業微信認証の有効化" + +#: settings/serializers/basic.py:9 +msgid "Subject" +msgstr "件名" + +#: settings/serializers/basic.py:13 +msgid "More url" +msgstr "もっとURL" + +#: settings/serializers/basic.py:30 +msgid "Site url" +msgstr "サイトURL" + +#: settings/serializers/basic.py:31 +msgid "eg: http://dev.jumpserver.org:8080" +msgstr "例えば: http://dev.jumpserver.org:8080" + +#: settings/serializers/basic.py:34 +msgid "User guide url" +msgstr "ユーザーガイドurl" + +#: settings/serializers/basic.py:35 +msgid "User first login update profile done redirect to it" +msgstr "ユーザーの最初のログイン更新プロファイルがリダイレクトされました" + +#: settings/serializers/basic.py:39 +msgid "" +"The forgot password url on login page, If you use ldap or cas external " +"authentication, you can set it" +msgstr "" +"ログインページでパスワードのURLを忘れてしまいました。ldapまたはcasの外部認証" +"を使用している場合は、設定できます。" + +#: settings/serializers/basic.py:43 +msgid "Global organization name" +msgstr "グローバル組織名" + +#: settings/serializers/basic.py:44 +msgid "The name of global organization to display" +msgstr "表示するグローバル組織の名前" + +#: settings/serializers/basic.py:46 +msgid "Enable announcement" +msgstr "アナウンスの有効化" + +#: settings/serializers/basic.py:47 +msgid "Announcement" +msgstr "発表" + +#: settings/serializers/basic.py:48 +msgid "Enable tickets" +msgstr "チケットを有効にする" + +#: settings/serializers/cleaning.py:8 +msgid "Period clean" +msgstr "定時清掃" + +#: settings/serializers/cleaning.py:12 +msgid "Login log keep days" +msgstr "ログインログは日数を保持します" + +#: settings/serializers/cleaning.py:12 settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:20 settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:28 +msgid "Unit: day" +msgstr "単位: 日" + +#: settings/serializers/cleaning.py:16 +msgid "Task log keep days" +msgstr "タスクログは日数を保持します" + +#: settings/serializers/cleaning.py:20 +msgid "Operate log keep days" +msgstr "ログ管理日を操作する" + +#: settings/serializers/cleaning.py:24 +msgid "FTP log keep days" +msgstr "ダウンロードのアップロード" + +#: settings/serializers/cleaning.py:28 +msgid "Cloud sync record keep days" +msgstr "クラウド同期レコードは日数を保持します" + +#: settings/serializers/cleaning.py:31 +msgid "Session keep duration" +msgstr "セッション維持期間" + +#: settings/serializers/cleaning.py:32 +msgid "" +"Unit: days, Session, record, command will be delete if more than duration, " +"only in database" +msgstr "" +"単位:日。セッション、録画、コマンドレコードがそれを超えると削除されます(デー" +"タベースストレージにのみ影響します。ossなどは影響しません」影響を受ける)" + +#: settings/serializers/email.py:21 +msgid "SMTP host" +msgstr "SMTPホスト" + +#: settings/serializers/email.py:22 +msgid "SMTP port" +msgstr "SMTPポート" + +#: settings/serializers/email.py:23 +msgid "SMTP account" +msgstr "SMTPアカウント" + +#: settings/serializers/email.py:25 +msgid "SMTP password" +msgstr "SMTPパスワード" + +#: settings/serializers/email.py:26 +msgid "Tips: Some provider use token except password" +msgstr "ヒント: 一部のプロバイダーはパスワード以外のトークンを使用します" + +#: settings/serializers/email.py:29 +msgid "Send user" +msgstr "ユーザーを送信" + +#: settings/serializers/email.py:30 +msgid "Tips: Send mail account, default SMTP account as the send account" +msgstr "" +"ヒント: 送信メールアカウント、送信アカウントとしてのデフォルトのSMTPアカウン" +"ト" + +#: settings/serializers/email.py:33 +msgid "Test recipient" +msgstr "テスト受信者" + +#: settings/serializers/email.py:34 +msgid "Tips: Used only as a test mail recipient" +msgstr "ヒント: テストメールの受信者としてのみ使用" + +#: settings/serializers/email.py:38 +msgid "If SMTP port is 465, may be select" +msgstr "SMTPポートが465の場合は、" + +#: settings/serializers/email.py:41 +msgid "Use TLS" +msgstr "TLSの使用" + +#: settings/serializers/email.py:42 +msgid "If SMTP port is 587, may be select" +msgstr "SMTPポートが587の場合は、" + +#: settings/serializers/email.py:45 +msgid "Subject prefix" +msgstr "件名プレフィックス" + +#: settings/serializers/email.py:54 +msgid "Create user email subject" +msgstr "ユーザーメール件名の作成" + +#: settings/serializers/email.py:55 +msgid "" +"Tips: When creating a user, send the subject of the email (eg:Create account " +"successfully)" +msgstr "" +"ヒント: ユーザーを作成するときに、メールの件名を送信します (例: アカウントを" +"正常に作成)" + +#: settings/serializers/email.py:59 +msgid "Create user honorific" +msgstr "ユーザー敬語の作成" + +#: settings/serializers/email.py:60 +msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" +msgstr "" +"ヒント: ユーザーを作成するときは、メールの敬語を送信します (例: こんにちは)" + +#: settings/serializers/email.py:64 +msgid "Create user email content" +msgstr "ユーザーのメールコンテンツを作成する" + +#: settings/serializers/email.py:65 +#, python-brace-format +msgid "" +"Tips: When creating a user, send the content of the email, support " +"{username} {name} {email} label" +msgstr "" +"ヒント:ユーザーの作成時にパスワード設定メールの内容を送信し、{username}{name}" +"{email}ラベルをサポートします。" + +#: settings/serializers/email.py:69 +msgid "Tips: Email signature (eg:jumpserver)" +msgstr "ヒント: メール署名 (例: jumpserver)" + +#: settings/serializers/other.py:6 +msgid "More..." +msgstr "詳細..." + +#: settings/serializers/other.py:9 +msgid "Email suffix" +msgstr "メールのサフィックス" + +#: settings/serializers/other.py:10 +msgid "" +"This is used by default if no email is returned during SSO authentication" +msgstr "これは、SSO認証中にメールが返されない場合にデフォルトで使用されます。" + +#: settings/serializers/other.py:14 +msgid "OTP issuer name" +msgstr "OTP発行者名" + +#: settings/serializers/other.py:18 +msgid "OTP valid window" +msgstr "OTP有効なウィンドウ" + +#: settings/serializers/other.py:23 +msgid "CMD" +msgstr "CMD" + +#: settings/serializers/other.py:24 +msgid "PowerShell" +msgstr "PowerShell" + +#: settings/serializers/other.py:26 +msgid "Shell (Windows)" +msgstr "シェル (Windows)" + +#: settings/serializers/other.py:27 +msgid "The shell type used when Windows assets perform ansible tasks" +msgstr "" +"Windowsアセットが実行可能なタスクを実行するときに使用されるシェルタイプ" + +#: settings/serializers/other.py:31 +msgid "Perm ungroup node" +msgstr "グループ化されていないノードを表示" + +#: settings/serializers/other.py:32 +msgid "Perm single to ungroup node" +msgstr "" +"グループ化されていないノードに個別に許可された資産を配置し、資産が存在する" +"ノードが表示されないようにしますが、そのノードが許可されていないという質問に" +"質問" + +#: settings/serializers/other.py:37 +msgid "Ticket authorize default time" +msgstr "デフォルト製造オーダ承認時間" + +#: settings/serializers/other.py:40 +msgid "day" +msgstr "日" + +#: settings/serializers/other.py:40 +msgid "hour" +msgstr "時" + +#: settings/serializers/other.py:41 +msgid "Ticket authorize default time unit" +msgstr "デフォルト製造オーダ承認時間単位" + +#: settings/serializers/other.py:44 +msgid "Help Docs URL" +msgstr "ドキュメントリンク" + +#: settings/serializers/other.py:45 +msgid "default: http://docs.jumpserver.org" +msgstr "デフォルト: http://docs.jumpserver.org" + +#: settings/serializers/other.py:49 +msgid "Help Support URL" +msgstr "サポートリンク" + +#: settings/serializers/other.py:50 +msgid "default: http://www.jumpserver.org/support/" +msgstr "デフォルト: http://www.jumpserver.org/support/" + +#: settings/serializers/security.py:10 +msgid "Password minimum length" +msgstr "パスワードの最小長" + +#: settings/serializers/security.py:14 +msgid "Admin user password minimum length" +msgstr "管理者ユーザーパスワードの最小長" + +#: settings/serializers/security.py:17 +msgid "Must contain capital" +msgstr "資本を含める必要があります" + +#: settings/serializers/security.py:20 +msgid "Must contain lowercase" +msgstr "小文字を含める必要があります。" + +#: settings/serializers/security.py:23 +msgid "Must contain numeric" +msgstr "数値を含める必要があります" + +#: settings/serializers/security.py:26 +msgid "Must contain special" +msgstr "特別な" + +#: settings/serializers/security.py:31 +msgid "" +"Unit: minute, If the user has failed to log in for a limited number of " +"times, no login is allowed during this time interval." +msgstr "" +"単位: 分。ユーザーが限られた回数だけログインできなかった場合、この時間間隔で" +"はログインはできません。" + +#: settings/serializers/security.py:40 +msgid "All users" +msgstr "すべてのユーザー" + +#: settings/serializers/security.py:41 +msgid "Only admin users" +msgstr "管理者のみ" + +#: settings/serializers/security.py:43 +msgid "Global MFA auth" +msgstr "グローバル有効化MFA認証" + +#: settings/serializers/security.py:47 +msgid "Third-party login users perform MFA authentication" +msgstr "サードパーティのログインユーザーがMFA認証を実行" + +#: settings/serializers/security.py:48 +msgid "The third-party login modes include OIDC, CAS, and SAML2" +msgstr "サードパーティのログインモードには、OIDC、CAS、SAML2" + +#: settings/serializers/security.py:52 +msgid "Limit the number of user login failures" +msgstr "ユーザーログインの失敗数を制限する" + +#: settings/serializers/security.py:56 +msgid "Block user login interval" +msgstr "ユーザーのログイン間隔をブロックする" + +#: settings/serializers/security.py:61 +msgid "Limit the number of IP login failures" +msgstr "IPログイン失敗の数を制限する" + +#: settings/serializers/security.py:65 +msgid "Block IP login interval" +msgstr "IPログイン間隔をブロックする" + +#: settings/serializers/security.py:69 +msgid "Login IP White List" +msgstr "ログインIPホワイトリスト" + +#: settings/serializers/security.py:74 +msgid "Login IP Black List" +msgstr "ログインIPブラックリスト" + +#: settings/serializers/security.py:80 +msgid "User password expiration" +msgstr "ユーザーパスワードの有効期限" + +#: settings/serializers/security.py:82 +msgid "" +"Unit: day, If the user does not update the password during the time, the " +"user password will expire failure;The password expiration reminder mail will " +"be automatic sent to the user by system within 5 days (daily) before the " +"password expires" +msgstr "" +"単位: 日。ユーザーがその期間中にパスワードを更新しなかった場合、ユーザーパス" +"ワードの有効期限が切れます。パスワードの有効期限が切れる前の5日 (毎日) 以内" +"に、パスワードの有効期限が切れるリマインダーメールがシステムからユーザーに自" +"動的に送信されます。" + +#: settings/serializers/security.py:89 +msgid "Number of repeated historical passwords" +msgstr "繰り返された履歴パスワードの数" + +#: settings/serializers/security.py:91 +msgid "" +"Tip: When the user resets the password, it cannot be the previous n " +"historical passwords of the user" +msgstr "" +"ヒント: ユーザーがパスワードをリセットすると、ユーザーの前のnの履歴パスワード" +"にすることはできません" + +#: settings/serializers/security.py:96 +msgid "Only single device login" +msgstr "単一デバイスログインのみ" + +#: settings/serializers/security.py:97 +msgid "Next device login, pre login will be logout" +msgstr "次のデバイスログイン、事前ログインはログアウトになります" + +#: settings/serializers/security.py:100 +msgid "Only exist user login" +msgstr "ユーザーログインのみ存在" + +#: settings/serializers/security.py:101 +msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet" +msgstr "Enableの場合、ユーザーがまだ存在しない場合、CAS、OIDC authは失敗します" + +#: settings/serializers/security.py:104 +msgid "Only from source login" +msgstr "ソースログインからのみ" + +#: settings/serializers/security.py:105 +msgid "Only log in from the user source property" +msgstr "ユーザーソースのプロパティからのみログイン" + +#: settings/serializers/security.py:109 +msgid "MFA verify TTL" +msgstr "MFAはTTLを確認します" + +#: settings/serializers/security.py:111 +msgid "" +"Unit: second, The verification MFA takes effect only when you view the " +"account password" +msgstr "" +"単位: 2番目に、検証MFAはアカウントのパスワードを表示したときにのみ有効になり" +"ます。" + +#: settings/serializers/security.py:116 +msgid "Enable Login dynamic code" +msgstr "ログイン動的コードの有効化" + +#: settings/serializers/security.py:117 +msgid "" +"The password and additional code are sent to a third party authentication " +"system for verification" +msgstr "" +"パスワードと追加コードは、検証のためにサードパーティの認証システムに送信され" +"ます" + +#: settings/serializers/security.py:122 +msgid "MFA in login page" +msgstr "ログインページのMFA" + +#: settings/serializers/security.py:123 +msgid "Eu security regulations(GDPR) require MFA to be on the login page" +msgstr "" +"Euセキュリティ規制 (GDPR) では、MFAがログインページにある必要があります" + +#: settings/serializers/security.py:126 +msgid "Enable Login captcha" +msgstr "ログインcaptchaの有効化" + +#: settings/serializers/security.py:127 +msgid "Enable captcha to prevent robot authentication" +msgstr "Captchaを有効にしてロボット認証を防止する" + +#: settings/serializers/security.py:146 +msgid "Security" +msgstr "セキュリティ" + +#: settings/serializers/security.py:149 +msgid "Enable terminal register" +msgstr "ターミナルレジスタの有効化" + +#: settings/serializers/security.py:151 +msgid "" +"Allow terminal register, after all terminal setup, you should disable this " +"for security" +msgstr "" +"ターミナルレジスタを許可し、すべてのターミナルセットアップの後、セキュリティ" +"のためにこれを無効にする必要があります" + +#: settings/serializers/security.py:155 +msgid "Enable watermark" +msgstr "透かしの有効化" + +#: settings/serializers/security.py:156 +msgid "Enabled, the web session and replay contains watermark information" +msgstr "Webセッションとリプレイには透かし情報が含まれています。" + +#: settings/serializers/security.py:160 +msgid "Connection max idle time" +msgstr "接続最大アイドル時間" + +#: settings/serializers/security.py:161 +msgid "If idle time more than it, disconnect connection Unit: minute" +msgstr "アイドル時間がそれ以上の場合は、接続単位を切断します: 分" + +#: settings/serializers/security.py:164 +msgid "Remember manual auth" +msgstr "手動入力パスワードの保存" + +#: settings/serializers/security.py:167 +msgid "Enable change auth secure mode" +msgstr "安全モードの変更を有効にする" + +#: settings/serializers/security.py:170 +msgid "Insecure command alert" +msgstr "安全でないコマンドアラート" + +#: settings/serializers/security.py:173 +msgid "Email recipient" +msgstr "メール受信者" + +#: settings/serializers/security.py:174 +msgid "Multiple user using , split" +msgstr "複数のユーザーを使用して、分割" + +#: settings/serializers/security.py:177 +msgid "Batch command execution" +msgstr "バッチコマンドの実行" + +#: settings/serializers/security.py:178 +msgid "Allow user run batch command or not using ansible" +msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" + +#: settings/serializers/security.py:181 +msgid "Session share" +msgstr "セッション共有" + +#: settings/serializers/security.py:182 +msgid "Enabled, Allows user active session to be shared with other users" +msgstr "" +"ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。" + +#: settings/serializers/security.py:185 +msgid "Remote Login Protection" +msgstr "リモートログイン保護" + +#: settings/serializers/security.py:187 +msgid "" +"The system determines whether the login IP address belongs to a common login " +"city. If the account is logged in from a common login city, the system sends " +"a remote login reminder" +msgstr "" +"システムは、ログインIPアドレスが共通のログイン都市に属しているかどうかを判断" +"します。アカウントが共通のログイン都市からログインしている場合、システムはリ" +"モートログインリマインダーを送信します" + +#: settings/serializers/terminal.py:15 +msgid "Auto" +msgstr "自動" + +#: settings/serializers/terminal.py:21 +msgid "Password auth" +msgstr "パスワード認証" + +#: settings/serializers/terminal.py:23 +msgid "Public key auth" +msgstr "鍵認証" + +#: settings/serializers/terminal.py:24 +msgid "" +"Tips: If use other auth method, like AD/LDAP, you should disable this to " +"avoid being able to log in after deleting" +msgstr "" +"ヒント: AD/LDAPなどの他の認証方法を使用する場合は、サードパーティ製システムの" +"削除後にこの項目を無効にする必要があります, ログインも可能" + +#: settings/serializers/terminal.py:28 +msgid "List sort by" +msgstr "リストの並べ替え" + +#: settings/serializers/terminal.py:31 +msgid "List page size" +msgstr "ページサイズを一覧表示" + +#: settings/serializers/terminal.py:34 +msgid "Telnet login regex" +msgstr "Telnetログインregex" + +#: settings/serializers/terminal.py:35 +msgid "" +"Tips: The login success message varies with devices. if you cannot log in to " +"the device through Telnet, set this parameter" +msgstr "" +"ヒント: ログイン成功メッセージはデバイスによって異なります。Telnet経由でデバ" +"イスにログインできない場合は、このパラメーターを設定します。" + +#: settings/serializers/terminal.py:38 +msgid "Enable database proxy" +msgstr "属性マップの有効化" + +#: settings/serializers/terminal.py:39 +msgid "Enable Razor" +msgstr "Razor の有効化" + +#: settings/serializers/terminal.py:40 +msgid "Enable SSH Client" +msgstr "SSH Clientの有効化" + +#: settings/serializers/terminal.py:51 +msgid "Default graphics resolution" +msgstr "デフォルトのグラフィック解像度" + +#: settings/serializers/terminal.py:52 +msgid "" +"Tip: Default resolution to use when connecting graphical assets in Luna pages" +msgstr "" +"ヒント: Luna ページでグラフィック アセットを接続するときに使用するデフォルト" +"の解像度" + +#: settings/utils/ldap.py:467 +msgid "ldap:// or ldaps:// protocol is used." +msgstr "ldap:// または ldaps:// プロトコルが使用されます。" + +#: settings/utils/ldap.py:478 +msgid "Host or port is disconnected: {}" +msgstr "ホストまたはポートが切断されました: {}" + +#: settings/utils/ldap.py:480 +msgid "The port is not the port of the LDAP service: {}" +msgstr "ポートはLDAPサービスのポートではありません: {}" + +#: settings/utils/ldap.py:482 +msgid "Please add certificate: {}" +msgstr "証明書を追加してください: {}" + +#: settings/utils/ldap.py:486 settings/utils/ldap.py:513 +#: settings/utils/ldap.py:543 settings/utils/ldap.py:571 +msgid "Unknown error: {}" +msgstr "不明なエラー: {}" + +#: settings/utils/ldap.py:500 +msgid "Bind DN or Password incorrect" +msgstr "DNまたはパスワードのバインドが正しくありません" + +#: settings/utils/ldap.py:507 +msgid "Please enter Bind DN: {}" +msgstr "バインドDN: {} を入力してください" + +#: settings/utils/ldap.py:509 +msgid "Please enter Password: {}" +msgstr "パスワードを入力してください: {}" + +#: settings/utils/ldap.py:511 +msgid "Please enter correct Bind DN and Password: {}" +msgstr "正しいバインドDNとパスワードを入力してください: {}" + +#: settings/utils/ldap.py:529 +msgid "Invalid User OU or User search filter: {}" +msgstr "無効なユーザー OU またはユーザー検索フィルター: {}" + +#: settings/utils/ldap.py:560 +msgid "LDAP User attr map not include: {}" +msgstr "LDAP ユーザーattrマップは含まれません: {}" + +#: settings/utils/ldap.py:567 +msgid "LDAP User attr map is not dict" +msgstr "LDAPユーザーattrマップはdictではありません" + +#: settings/utils/ldap.py:586 +msgid "LDAP authentication is not enabled" +msgstr "LDAP 認証が有効になっていない" + +#: settings/utils/ldap.py:604 +msgid "Error (Invalid LDAP server): {}" +msgstr "エラー (LDAPサーバーが無効): {}" + +#: settings/utils/ldap.py:606 +msgid "Error (Invalid Bind DN): {}" +msgstr "エラー (DNのバインドが無効): {}" + +#: settings/utils/ldap.py:608 +msgid "Error (Invalid LDAP User attr map): {}" +msgstr "エラー (LDAPユーザーattrマップが無効): {}" + +#: settings/utils/ldap.py:610 +msgid "Error (Invalid User OU or User search filter): {}" +msgstr "エラー (ユーザーOUまたはユーザー検索フィルターが無効): {}" + +#: settings/utils/ldap.py:612 +msgid "Error (Not enabled LDAP authentication): {}" +msgstr "エラー (LDAP認証が有効化されていません): {}" + +#: settings/utils/ldap.py:614 +msgid "Error (Unknown): {}" +msgstr "エラー (不明): {}" + +#: settings/utils/ldap.py:617 +msgid "Succeed: Match {} s user" +msgstr "成功: {} 人のユーザーに一致" + +#: settings/utils/ldap.py:650 +msgid "Authentication failed (configuration incorrect): {}" +msgstr "認証に失敗しました (設定が正しくありません): {}" + +#: settings/utils/ldap.py:654 +msgid "Authentication failed (username or password incorrect): {}" +msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}" + +#: settings/utils/ldap.py:656 +msgid "Authentication failed (Unknown): {}" +msgstr "認証に失敗しました (不明): {}" + +#: settings/utils/ldap.py:659 +msgid "Authentication success: {}" +msgstr "認証成功: {}" + +#: templates/_csv_import_export.html:8 +msgid "Export" +msgstr "エクスポート" + +#: templates/_csv_import_export.html:13 templates/_csv_import_modal.html:5 +msgid "Import" +msgstr "インポート" + +#: templates/_csv_import_modal.html:12 +msgid "Download the imported template or use the exported CSV file format" +msgstr "" +"インポートしたテンプレートをダウンロードするか、エクスポートしたCSVファイル形" +"式を使用する" + +#: templates/_csv_import_modal.html:13 +msgid "Download the import template" +msgstr "インポートテンプレートのダウンロード" + +#: templates/_csv_import_modal.html:17 templates/_csv_update_modal.html:17 +msgid "Select the CSV file to import" +msgstr "インポートするCSVファイルの選択" + +#: templates/_csv_import_modal.html:39 templates/_csv_update_modal.html:42 +msgid "Please select file" +msgstr "ファイルを選択してください" + +#: templates/_csv_update_modal.html:12 +msgid "Download the update template or use the exported CSV file format" +msgstr "" +"更新テンプレートをダウンロードするか、エクスポートしたCSVファイル形式を使用す" +"る" + +#: templates/_csv_update_modal.html:13 +msgid "Download the update template" +msgstr "更新テンプレートのダウンロード" + +#: templates/_header_bar.html:12 +msgid "Help" +msgstr "ヘルプ" + +#: templates/_header_bar.html:19 +msgid "Docs" +msgstr "ドキュメント" + +#: templates/_header_bar.html:25 +msgid "Commercial support" +msgstr "商用サポート" + +#: templates/_header_bar.html:76 users/forms/profile.py:44 +msgid "Profile" +msgstr "プロフィール" + +#: templates/_header_bar.html:79 +msgid "Admin page" +msgstr "ページの管理" + +#: templates/_header_bar.html:81 +msgid "User page" +msgstr "ユーザーページ" + +#: templates/_header_bar.html:84 +msgid "API Key" +msgstr "API Key" + +#: templates/_header_bar.html:85 +msgid "Logout" +msgstr "ログアウト" + +#: templates/_message.html:6 +msgid "" +"\n" +" Your account has expired, please contact the administrator.\n" +" " +msgstr "" +"\n" +" アカウントが期限切れになったので、管理者に連絡してくださ" +"い。 " + +#: templates/_message.html:13 +msgid "Your account will at" +msgstr "あなたのアカウントは" + +#: templates/_message.html:13 templates/_message.html:30 +msgid "expired. " +msgstr "期限切れです。" + +#: templates/_message.html:23 +#, python-format +msgid "" +"\n" +" Your password has expired, please click this link update password.\n" +" " +msgstr "" +"\n" +" パスワードが期限切れになりましたので、クリックしてください " +" リンク パスワードの更新\n" +" " + +#: templates/_message.html:30 +msgid "Your password will at" +msgstr "あなたのパスワードは" + +#: templates/_message.html:31 +#, python-format +msgid "" +"\n" +" please click this " +"link to update your password.\n" +" " +msgstr "" +"\n" +" クリックしてください リンク パスワードの更新\n" +" " + +#: templates/_message.html:43 +#, python-format +msgid "" +"\n" +" Your information was incomplete. Please click this link to complete your information.\n" +" " +msgstr "" +"\n" +" あなたの情報が不完全なので、クリックしてください。 リンク 補完\n" +" " + +#: templates/_message.html:56 +#, python-format +msgid "" +"\n" +" Your ssh public key not set or expired. Please click this link to update\n" +" " +msgstr "" +"\n" +" SSHキーが設定されていないか無効になっている場合は、 リンク 更新\n" +" " + +#: templates/_mfa_login_field.html:28 +msgid "Send verification code" +msgstr "確認コードを送信" + +#: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:129 +msgid "Wait: " +msgstr "待つ:" + +#: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:145 +msgid "The verification code has been sent" +msgstr "確認コードが送信されました" + +#: templates/_without_nav_base.html:26 +msgid "Home page" +msgstr "ホームページ" + +#: templates/resource_download.html:18 templates/resource_download.html:31 +msgid "Client" +msgstr "クライアント" + +#: templates/resource_download.html:20 +msgid "" +"JumpServer Client, currently used to launch the client, now only support " +"launch RDP SSH client, The Telnet client will next" +msgstr "" +"JumpServerクライアントは、現在特定のクライアントプログラムの接続資産を喚起す" +"るために使用されており、現在はRDP SSHクライアントのみをサポートしています。" +"「Telnetは将来的にサポートする" + +#: templates/resource_download.html:31 +msgid "Microsoft" +msgstr "マイクロソフト" + +#: templates/resource_download.html:31 +msgid "Official" +msgstr "公式" + +#: templates/resource_download.html:33 +msgid "" +"macOS needs to download the client to connect RDP asset, which comes with " +"Windows" +msgstr "" +"MacOSは、Windowsに付属のRDPアセットを接続するためにクライアントをダウンロード" +"する必要があります" + +#: templates/resource_download.html:42 +msgid "Windows Remote application publisher tools" +msgstr "Windowsリモートアプリケーション発行者ツール" + +#: templates/resource_download.html:43 +msgid "" +"OpenSSH is a program used to connect remote applications in the Windows " +"Remote Application Publisher" +msgstr "" +"OpenSSHはリモートアプリケーションをWindowsリモートアプリケーションで接続する" +"プログラムです" + +#: templates/resource_download.html:48 +msgid "" +"Jmservisor is the program used to pull up remote applications in Windows " +"Remote Application publisher" +msgstr "" +"Jmservisorはwindowsリモートアプリケーションパブリケーションサーバでリモートア" +"プリケーションを引き出すためのプログラムです" + +#: templates/resource_download.html:57 +msgid "Offline video player" +msgstr "オフラインビデオプレーヤー" + +#: terminal/api/component/endpoint.py:31 +msgid "Not found protocol query params" +msgstr "プロトコルクエリパラメータが見つかりません" + +#: terminal/api/component/storage.py:28 +msgid "Deleting the default storage is not allowed" +msgstr "デフォルトのストレージの削除は許可されていません" + +#: terminal/api/component/storage.py:31 +msgid "Cannot delete storage that is being used" +msgstr "使用中のストレージを削除できません" + +#: terminal/api/component/storage.py:72 terminal/api/component/storage.py:73 +msgid "Command storages" +msgstr "コマンドストア" + +#: terminal/api/component/storage.py:79 +msgid "Invalid" +msgstr "無効" + +#: terminal/api/component/storage.py:119 +msgid "Test failure: {}" +msgstr "テスト失敗: {}" + +#: terminal/api/component/storage.py:122 +msgid "Test successful" +msgstr "テスト成功" + +#: terminal/api/component/storage.py:124 +msgid "Test failure: Account invalid" +msgstr "テスト失敗: アカウントが無効" + +#: terminal/api/component/terminal.py:35 +msgid "Have online sessions" +msgstr "オンラインセッションを持つ" + +#: terminal/api/session/session.py:217 +msgid "Session does not exist: {}" +msgstr "セッションが存在しません: {}" + +#: terminal/api/session/session.py:220 +msgid "Session is finished or the protocol not supported" +msgstr "セッションが終了したか、プロトコルがサポートされていません" + +#: terminal/api/session/session.py:233 +msgid "User does not have permission" +msgstr "ユーザーに権限がありません" + +#: terminal/api/session/sharing.py:29 +msgid "Secure session sharing settings is disabled" +msgstr "安全なセッション共有設定が無効になっています" + +#: terminal/apps.py:9 +msgid "Terminals" +msgstr "ターミナル管理" + +#: terminal/backends/command/models.py:16 +msgid "Ordinary" +msgstr "普通" + +#: terminal/backends/command/models.py:17 +msgid "Dangerous" +msgstr "危険" + +#: terminal/backends/command/models.py:23 +msgid "Input" +msgstr "入力" + +#: terminal/backends/command/models.py:24 +#: terminal/backends/command/serializers.py:38 +msgid "Output" +msgstr "出力" + +#: terminal/backends/command/models.py:25 terminal/models/session/replay.py:9 +#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77 +#: terminal/templates/terminal/_msg_command_alert.html:10 +#: tickets/models/ticket/command_confirm.py:15 +msgid "Session" +msgstr "セッション" + +#: terminal/backends/command/models.py:26 +#: terminal/backends/command/serializers.py:18 +msgid "Risk level" +msgstr "リスクレベル" + +#: terminal/backends/command/serializers.py:16 +msgid "Session ID" +msgstr "セッションID" + +#: terminal/backends/command/serializers.py:37 +#, fuzzy +msgid "Account " +msgstr "アカウント" + +#: terminal/backends/command/serializers.py:39 +msgid "Timestamp" +msgstr "タイムスタンプ" + +#: terminal/backends/command/serializers.py:41 +#: terminal/models/component/terminal.py:84 +msgid "Remote Address" +msgstr "リモートアドレス" + +#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 +#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 +#: terminal/connect_methods.py:50 +#, fuzzy +msgid "DB Client" +msgstr "クライアント" + +#: terminal/const.py:30 +msgid "Critical" +msgstr "クリティカル" + +#: terminal/const.py:31 +msgid "High" +msgstr "高い" + +#: terminal/const.py:32 terminal/serializers/session.py:17 +#: users/templates/users/reset_password.html:50 +msgid "Normal" +msgstr "正常" + +#: terminal/const.py:33 +msgid "Offline" +msgstr "オフライン" + +#: terminal/exceptions.py:8 +msgid "Bulk create not support" +msgstr "一括作成非サポート" + +#: terminal/exceptions.py:13 +msgid "Storage is invalid" +msgstr "ストレージが無効です" + +#: terminal/models/applet/applet.py:25 +#, fuzzy +msgid "Author" +msgstr "資産アカウント" + +#: terminal/models/applet/applet.py:30 +msgid "Tags" +msgstr "" + +#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +msgid "Hosts" +msgstr "ホスト" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +#, fuzzy +msgid "Deploy options" +msgstr "その他のログインオプション" + +#: terminal/models/applet/host.py:19 +msgid "Inited" +msgstr "" + +#: terminal/models/applet/host.py:20 +#, fuzzy +msgid "Date inited" +msgstr "終了日" + +#: terminal/models/applet/host.py:21 +#, fuzzy +msgid "Date synced" +msgstr "日付の同期" + +#: terminal/models/applet/host.py:33 +msgid "Applet host" +msgstr "リモートアプリケーションパブリッシャ" + +#: terminal/models/applet/host.py:105 +#, fuzzy +msgid "Hosting" +msgstr "ホスト" + +#: terminal/models/applet/host.py:106 +msgid "Initial" +msgstr "" + +#: terminal/models/component/endpoint.py:15 +msgid "HTTPS Port" +msgstr "HTTPS ポート" + +#: terminal/models/component/endpoint.py:16 +msgid "HTTP Port" +msgstr "HTTP ポート" + +#: terminal/models/component/endpoint.py:17 +msgid "SSH Port" +msgstr "SSH ポート" + +#: terminal/models/component/endpoint.py:18 +msgid "RDP Port" +msgstr "RDP ポート" + +#: terminal/models/component/endpoint.py:25 +#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 +#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 +#: terminal/serializers/storage.py:98 +msgid "Endpoint" +msgstr "エンドポイント" + +#: terminal/models/component/endpoint.py:87 +msgid "IP group" +msgstr "IP グループ" + +#: terminal/models/component/endpoint.py:99 +msgid "Endpoint rule" +msgstr "エンドポイントルール" + +#: terminal/models/component/status.py:14 +msgid "Session Online" +msgstr "セッションオンライン" + +#: terminal/models/component/status.py:15 +msgid "CPU Load" +msgstr "CPUロード" + +#: terminal/models/component/status.py:16 +msgid "Memory Used" +msgstr "使用メモリ" + +#: terminal/models/component/status.py:17 +msgid "Disk Used" +msgstr "使用済みディスク" + +#: terminal/models/component/status.py:18 +msgid "Connections" +msgstr "接続" + +#: terminal/models/component/status.py:19 +msgid "Threads" +msgstr "スレッド" + +#: terminal/models/component/status.py:20 +msgid "Boot Time" +msgstr "ブート時間" + +#: terminal/models/component/storage.py:28 +msgid "Default storage" +msgstr "デフォルトのストレージ" + +#: terminal/models/component/storage.py:140 +#: terminal/models/component/terminal.py:85 +msgid "Command storage" +msgstr "コマンドストレージ" + +#: terminal/models/component/storage.py:200 +#: terminal/models/component/terminal.py:86 +msgid "Replay storage" +msgstr "再生ストレージ" + +#: terminal/models/component/terminal.py:82 +msgid "type" +msgstr "タイプ" + +#: terminal/models/component/terminal.py:87 +msgid "Application User" +msgstr "ユーザーの適用" + +#: terminal/models/component/terminal.py:158 +msgid "Can view terminal config" +msgstr "ターミナル構成を表示できます" + +#: terminal/models/session/command.py:66 +msgid "Command record" +msgstr "コマンドレコード" + +#: terminal/models/session/replay.py:12 +msgid "Session replay" +msgstr "セッション再生" + +#: terminal/models/session/replay.py:14 +msgid "Can upload session replay" +msgstr "セッションのリプレイをアップロードできます" + +#: terminal/models/session/replay.py:15 +msgid "Can download session replay" +msgstr "セッション再生をダウンロードできます" + +#: terminal/models/session/session.py:35 terminal/models/session/sharing.py:100 +msgid "Login from" +msgstr "ログイン元" + +#: terminal/models/session/session.py:40 +msgid "Replay" +msgstr "リプレイ" + +#: terminal/models/session/session.py:44 +msgid "Date end" +msgstr "終了日" + +#: terminal/models/session/session.py:237 +msgid "Session record" +msgstr "セッション記録" + +#: terminal/models/session/session.py:239 +msgid "Can monitor session" +msgstr "セッションを監視できます" + +#: terminal/models/session/session.py:240 +msgid "Can share session" +msgstr "セッションを共有できます" + +#: terminal/models/session/session.py:241 +msgid "Can terminate session" +msgstr "セッションを終了できます" + +#: terminal/models/session/session.py:242 +msgid "Can validate session action perm" +msgstr "セッションアクションのパーマを検証できます" + +#: terminal/models/session/sharing.py:30 +msgid "Expired time (min)" +msgstr "期限切れ時間 (分)" + +#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82 +msgid "Session sharing" +msgstr "セッション共有" + +#: terminal/models/session/sharing.py:38 +msgid "Can add super session sharing" +msgstr "スーパーセッション共有を追加できます" + +#: terminal/models/session/sharing.py:65 +msgid "Link not active" +msgstr "リンクがアクティブでない" + +#: terminal/models/session/sharing.py:67 +msgid "Link expired" +msgstr "リンク期限切れ" + +#: terminal/models/session/sharing.py:69 +msgid "User not allowed to join" +msgstr "ユーザーはセッションに参加できません" + +#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59 +msgid "Joiner" +msgstr "ジョイナー" + +#: terminal/models/session/sharing.py:89 +msgid "Date joined" +msgstr "参加日" + +#: terminal/models/session/sharing.py:92 +msgid "Date left" +msgstr "日付が残っています" + +#: terminal/models/session/sharing.py:115 +msgid "Session join record" +msgstr "セッション参加記録" + +#: terminal/models/session/sharing.py:131 +msgid "Invalid verification code" +msgstr "検証コードが無効" + +#: terminal/notifications.py:22 +msgid "Sessions" +msgstr "セッション" + +#: terminal/notifications.py:68 +msgid "Danger command alert" +msgstr "危険コマンドアラート" + +#: terminal/notifications.py:95 terminal/notifications.py:143 +msgid "Level" +msgstr "レベル" + +#: terminal/notifications.py:113 +msgid "Batch danger command alert" +msgstr "一括危険コマンド警告" + +#: terminal/serializers/applet.py:16 +#, fuzzy +msgid "Published" +msgstr "公開キー" + +#: terminal/serializers/applet.py:17 +#, fuzzy +msgid "Unpublished" +msgstr "終了" + +#: terminal/serializers/applet.py:18 +#, fuzzy +msgid "Not match" +msgstr "ユーザーにマッチしなかった" + +#: terminal/serializers/applet.py:32 +msgid "Icon" +msgstr "" + +#: terminal/serializers/applet_host.py:21 +#, fuzzy +msgid "Per Session" +msgstr "セッション" + +#: terminal/serializers/applet_host.py:22 +msgid "Per Device" +msgstr "" + +#: terminal/serializers/applet_host.py:28 +#, fuzzy +msgid "RDS Licensing" +msgstr "ライセンス" + +#: terminal/serializers/applet_host.py:29 +msgid "RDS License Server" +msgstr "" + +#: terminal/serializers/applet_host.py:30 +msgid "RDS Licensing Mode" +msgstr "" + +#: terminal/serializers/applet_host.py:32 +msgid "RDS fSingleSessionPerUser" +msgstr "" + +#: terminal/serializers/applet_host.py:33 +msgid "RDS Max Disconnection Time" +msgstr "" + +#: terminal/serializers/applet_host.py:34 +msgid "RDS Remote App Logoff Time Limit" +msgstr "" + +#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +msgid "Load status" +msgstr "ロードステータス" + +#: terminal/serializers/endpoint.py:14 +msgid "Magnus listen db port" +msgstr "Magnus がリッスンするデータベース ポート" + +#: terminal/serializers/endpoint.py:17 +msgid "Magnus Listen port range" +msgstr "Magnus がリッスンするポート範囲" + +#: terminal/serializers/endpoint.py:19 +msgid "" +"The range of ports that Magnus listens on is modified in the configuration " +"file" +msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" + +#: terminal/serializers/endpoint.py:51 +msgid "" +"If asset IP addresses under different endpoints conflict, use asset labels" +msgstr "" +"異なるエンドポイントの下に競合するアセットIPがある場合は、アセットタグを使用" +"して実装します" + +#: terminal/serializers/session.py:18 +msgid "Tunnel" +msgstr "" + +#: terminal/serializers/session.py:41 +msgid "User ID" +msgstr "ユーザーID" + +#: terminal/serializers/session.py:42 +msgid "Asset ID" +msgstr "資産ID" + +#: terminal/serializers/session.py:43 +msgid "Login from display" +msgstr "表示からのログイン" + +#: terminal/serializers/session.py:45 +msgid "Can replay" +msgstr "再生できます" + +#: terminal/serializers/session.py:46 +msgid "Can join" +msgstr "参加できます" + +#: terminal/serializers/session.py:47 +msgid "Terminal ID" +msgstr "ターミナル ID" + +#: terminal/serializers/session.py:48 +msgid "Is finished" +msgstr "終了しました" + +#: terminal/serializers/session.py:49 +msgid "Can terminate" +msgstr "終了できます" + +#: terminal/serializers/session.py:50 +msgid "Terminal display" +msgstr "ターミナルディスプレイ" + +#: terminal/serializers/session.py:55 +msgid "Command amount" +msgstr "コマンド量" + +#: terminal/serializers/storage.py:20 +msgid "Endpoint invalid: remove path `{}`" +msgstr "エンドポイントが無効: パス '{}' を削除" + +#: terminal/serializers/storage.py:26 +msgid "Bucket" +msgstr "バケット" + +#: terminal/serializers/storage.py:30 +#: xpack/plugins/cloud/serializers/account_attrs.py:17 +msgid "Access key id" +msgstr "アクセスキー" + +#: terminal/serializers/storage.py:34 +#: xpack/plugins/cloud/serializers/account_attrs.py:20 +msgid "Access key secret" +msgstr "アクセスキーシークレット" + +#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:217 +msgid "Region" +msgstr "リージョン" + +#: terminal/serializers/storage.py:109 +msgid "Container name" +msgstr "コンテナー名" + +#: terminal/serializers/storage.py:112 +msgid "Account key" +msgstr "アカウントキー" + +#: terminal/serializers/storage.py:115 +msgid "Endpoint suffix" +msgstr "エンドポイントサフィックス" + +#: terminal/serializers/storage.py:135 +msgid "The address format is incorrect" +msgstr "アドレス形式が正しくありません" + +#: terminal/serializers/storage.py:142 +msgid "Host invalid" +msgstr "ホスト無効" + +#: terminal/serializers/storage.py:145 +msgid "Port invalid" +msgstr "ポートが無効" + +#: terminal/serializers/storage.py:160 +msgid "Index by date" +msgstr "日付による索引付け" + +#: terminal/serializers/storage.py:161 +msgid "Whether to create an index by date" +msgstr "現在の日付に基づいてインデックスを動的に作成するかどうか" + +#: terminal/serializers/storage.py:164 +msgid "Index" +msgstr "インデックス" + +#: terminal/serializers/storage.py:166 +msgid "Doc type" +msgstr "Docタイプ" + +#: terminal/serializers/storage.py:168 +msgid "Ignore Certificate Verification" +msgstr "証明書の検証を無視する" + +#: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 +msgid "Not found" +msgstr "見つかりません" + +#: terminal/templates/terminal/_msg_command_alert.html:10 +msgid "view" +msgstr "表示" + +#: terminal/utils/db_port_mapper.py:77 +msgid "" +"No available port is matched. The number of databases may have exceeded the " +"number of ports open to the database agent service, Contact the " +"administrator to open more ports." +msgstr "" +"利用可能なポートと一致しません。データベースの数が、データベース プロキシ " +"サービスによって開かれたポートの数を超えた可能性があります。さらにポートを開" +"くには、管理者に連絡してください。" + +#: terminal/utils/db_port_mapper.py:103 +msgid "" +"No ports can be used, check and modify the limit on the number of ports that " +"Magnus listens on in the configuration file." +msgstr "" +"使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の" +"制限を確認して変更してください. " + +#: terminal/utils/db_port_mapper.py:105 +msgid "All available port count: {}, Already use port count: {}" +msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" + +#: tickets/apps.py:7 +msgid "Tickets" +msgstr "チケット" + +#: tickets/const.py:9 +msgid "Apply for asset" +msgstr "資産の申請" + +#: tickets/const.py:16 tickets/const.py:24 tickets/const.py:43 +msgid "Open" +msgstr "オープン" + +#: tickets/const.py:18 tickets/const.py:31 +msgid "Reopen" +msgstr "" + +#: tickets/const.py:19 tickets/const.py:32 +msgid "Approved" +msgstr "承認済み" + +#: tickets/const.py:20 tickets/const.py:33 +msgid "Rejected" +msgstr "拒否" + +#: tickets/const.py:30 tickets/const.py:38 +msgid "Closed" +msgstr "クローズ" + +#: tickets/const.py:46 +msgid "Approve" +msgstr "承認" + +#: tickets/const.py:50 +msgid "One level" +msgstr "1つのレベル" + +#: tickets/const.py:51 +msgid "Two level" +msgstr "2つのレベル" + +#: tickets/const.py:55 +msgid "Org admin" +msgstr "Org admin" + +#: tickets/const.py:56 +msgid "Custom user" +msgstr "カスタムユーザー" + +#: tickets/const.py:57 +msgid "Super admin" +msgstr "スーパー管理者" + +#: tickets/const.py:58 +msgid "Super admin and org admin" +msgstr "スーパーadminとorg admin" + +#: tickets/errors.py:9 +msgid "Ticket already closed" +msgstr "チケットはすでに閉じています" + +#: tickets/handlers/apply_asset.py:36 +msgid "" +"Created by the ticket ticket title: {} ticket applicant: {} ticket " +"processor: {} ticket ID: {}" +msgstr "" +"チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケットID: " +"{}" + +#: tickets/handlers/base.py:86 +msgid "Change field" +msgstr "フィールドを変更" + +#: tickets/handlers/base.py:86 +msgid "Before change" +msgstr "変更前" + +#: tickets/handlers/base.py:86 +msgid "After change" +msgstr "変更後" + +#: tickets/handlers/base.py:98 +msgid "{} {} the ticket" +msgstr "{} {} チケット" + +#: tickets/models/comment.py:14 +msgid "common" +msgstr "" + +#: tickets/models/comment.py:23 +msgid "User display name" +msgstr "ユーザー表示名" + +#: tickets/models/comment.py:24 +msgid "Body" +msgstr "ボディ" + +#: tickets/models/flow.py:19 tickets/models/flow.py:61 +#: tickets/models/ticket/general.py:40 +msgid "Approve level" +msgstr "レベルを承認する" + +#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +msgid "Approve strategy" +msgstr "戦略を承認する" + +#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +msgid "Assignees" +msgstr "アシニーズ" + +#: tickets/models/flow.py:33 +msgid "Ticket flow approval rule" +msgstr "チケットフロー承認ルール" + +#: tickets/models/flow.py:66 +msgid "Ticket flow" +msgstr "チケットの流れ" + +#: tickets/models/relation.py:10 +msgid "Ticket session relation" +msgstr "チケットセッションの関係" + +#: tickets/models/ticket/apply_application.py:10 +#: tickets/models/ticket/apply_asset.py:13 +msgid "Permission name" +msgstr "認可ルール名" + +#: tickets/models/ticket/apply_application.py:19 +msgid "Apply applications" +msgstr "アプリケーションの適用" + +#: tickets/models/ticket/apply_application.py:22 +msgid "Apply system users" +msgstr "システムユーザーの適用" + +#: tickets/models/ticket/apply_asset.py:9 +#: tickets/serializers/ticket/apply_asset.py:14 +msgid "Select at least one asset or node" +msgstr "少なくとも1つのアセットまたはノードを選択します。" + +#: tickets/models/ticket/apply_asset.py:14 +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "ノードの適用" + +#: tickets/models/ticket/apply_asset.py:16 +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申請資産" + +#: tickets/models/ticket/apply_asset.py:17 +#, fuzzy +msgid "Apply accounts" +msgstr "アプリケーションアカウント" + +#: tickets/models/ticket/command_confirm.py:9 +msgid "Run user" +msgstr "ユーザーの実行" + +#: tickets/models/ticket/command_confirm.py:11 +msgid "Run asset" +msgstr "アセットの実行" + +#: tickets/models/ticket/command_confirm.py:12 +msgid "Run command" +msgstr "実行コマンド" + +#: tickets/models/ticket/command_confirm.py:19 +#, fuzzy +#| msgid "Command filter" +msgid "Command filter acl" +msgstr "コマンドフィルター" + +#: tickets/models/ticket/general.py:75 +msgid "Ticket step" +msgstr "チケットステップ" + +#: tickets/models/ticket/general.py:93 +msgid "Ticket assignee" +msgstr "割り当てられたチケット" + +#: tickets/models/ticket/general.py:272 +msgid "Title" +msgstr "タイトル" + +#: tickets/models/ticket/general.py:288 +msgid "Applicant" +msgstr "応募者" + +#: tickets/models/ticket/general.py:292 +msgid "TicketFlow" +msgstr "作業指示プロセス" + +#: tickets/models/ticket/general.py:295 +msgid "Approval step" +msgstr "承認ステップ" + +#: tickets/models/ticket/general.py:298 +msgid "Relation snapshot" +msgstr "製造オーダスナップショット" + +#: tickets/models/ticket/general.py:392 +msgid "Please try again" +msgstr "もう一度お試しください" + +#: tickets/models/ticket/general.py:425 +msgid "Super ticket" +msgstr "スーパーチケット" + +#: tickets/models/ticket/login_asset_confirm.py:11 +msgid "Login user" +msgstr "ログインユーザー" + +#: tickets/models/ticket/login_asset_confirm.py:14 +msgid "Login asset" +msgstr "ログイン資産" + +#: tickets/models/ticket/login_asset_confirm.py:17 +#, fuzzy +msgid "Login account" +msgstr "ログインacl" + +#: tickets/models/ticket/login_confirm.py:12 +msgid "Login datetime" +msgstr "ログイン日時" + +#: tickets/notifications.py:63 +msgid "Ticket basic info" +msgstr "チケット基本情報" + +#: tickets/notifications.py:64 +msgid "Ticket applied info" +msgstr "チケット適用情報" + +#: tickets/notifications.py:109 +msgid "Your has a new ticket, applicant - {}" +msgstr "新しいチケットがあります- {}" + +#: tickets/notifications.py:113 +msgid "{}: New Ticket - {} ({})" +msgstr "新しいチケット- {} ({})" + +#: tickets/notifications.py:157 +msgid "Your ticket has been processed, processor - {}" +msgstr "チケットが処理されました。プロセッサー- {}" + +#: tickets/notifications.py:161 +msgid "Ticket has processed - {} ({})" +msgstr "チケットが処理済み- {} ({})" + +#: tickets/serializers/flow.py:21 +msgid "Assignees display" +msgstr "受付者名" + +#: tickets/serializers/flow.py:47 +msgid "Please select the Assignees" +msgstr "受付をお選びください" + +#: tickets/serializers/flow.py:75 +msgid "The current organization type already exists" +msgstr "現在の組織タイプは既に存在します。" + +#: tickets/serializers/super_ticket.py:11 +msgid "Processor" +msgstr "プロセッサ" + +#: tickets/serializers/ticket/apply_asset.py:16 +msgid "Support fuzzy search, and display up to 10 items" +msgstr "ファジー検索をサポートし、最大10項目を表示します。" + +#: tickets/serializers/ticket/apply_asset.py:28 +msgid "Apply actions" +msgstr "申請アクション" + +#: tickets/serializers/ticket/common.py:15 +#: tickets/serializers/ticket/common.py:77 +msgid "Created by ticket ({}-{})" +msgstr "チケットで作成 ({}-{})" + +#: tickets/serializers/ticket/common.py:67 +msgid "The expiration date should be greater than the start date" +msgstr "有効期限は開始日より大きくする必要があります" + +#: tickets/serializers/ticket/common.py:84 +msgid "Permission named `{}` already exists" +msgstr "'{}'という名前の権限は既に存在します" + +#: tickets/serializers/ticket/ticket.py:95 +msgid "The ticket flow `{}` does not exist" +msgstr "チケットフロー '{}'が存在しない" + +#: tickets/templates/tickets/_msg_ticket.html:20 +msgid "View details" +msgstr "詳細の表示" + +#: tickets/templates/tickets/_msg_ticket.html:25 +msgid "Direct approval" +msgstr "直接承認" + +#: tickets/templates/tickets/approve_check_password.html:11 +msgid "Ticket information" +msgstr "作業指示情報" + +#: tickets/templates/tickets/approve_check_password.html:29 +#: tickets/views/approve.py:38 +msgid "Ticket approval" +msgstr "作業指示の承認" + +#: tickets/templates/tickets/approve_check_password.html:45 +msgid "Approval" +msgstr "承認" + +#: tickets/templates/tickets/approve_check_password.html:54 +msgid "Go Login" +msgstr "ログイン" + +#: tickets/views/approve.py:39 +msgid "" +"This ticket does not exist, the process has ended, or this link has expired" +msgstr "" +"このワークシートが存在しないか、ワークシートが終了したか、このリンクが無効に" +"なっています" + +#: tickets/views/approve.py:68 +msgid "Click the button below to approve or reject" +msgstr "下のボタンをクリックして同意または拒否。" + +#: tickets/views/approve.py:70 +msgid "After successful authentication, this ticket can be approved directly" +msgstr "認証に成功した後、作業指示書は直接承認することができる。" + +#: tickets/views/approve.py:92 +msgid "Illegal approval action" +msgstr "無効な承認アクション" + +#: tickets/views/approve.py:105 +msgid "This user is not authorized to approve this ticket" +msgstr "このユーザーはこの作業指示を承認する権限がありません" + +#: users/api/user.py:183 +msgid "Could not reset self otp, use profile reset instead" +msgstr "自己otpをリセットできませんでした、代わりにプロファイルリセットを使用" + +#: users/apps.py:9 +msgid "Users" +msgstr "ユーザー" + +#: users/const.py:10 +msgid "System administrator" +msgstr "システム管理者" + +#: users/const.py:11 +msgid "System auditor" +msgstr "システム監査人" + +#: users/const.py:12 +msgid "Organization administrator" +msgstr "組織管理者" + +#: users/const.py:13 +msgid "Organization auditor" +msgstr "組織監査人" + +#: users/const.py:18 +msgid "Reset link will be generated and sent to the user" +msgstr "リセットリンクが生成され、ユーザーに送信されます" + +#: users/const.py:19 +msgid "Set password" +msgstr "パスワードの設定" + +#: users/exceptions.py:10 +msgid "MFA not enabled" +msgstr "MFAが有効化されていません" + +#: users/exceptions.py:20 +msgid "MFA method not support" +msgstr "MFAメソッドはサポートしていません" + +#: users/forms/profile.py:50 +msgid "" +"When enabled, you will enter the MFA binding process the next time you log " +"in. you can also directly bind in \"personal information -> quick " +"modification -> change MFA Settings\"!" +msgstr "" +"有効にすると、次回のログイン時にマルチファクタ認証バインドプロセスに入りま" +"す。(個人情報->クイック修正->MFAマルチファクタ認証の設定)で直接バインド!" + +#: users/forms/profile.py:61 +msgid "* Enable MFA to make the account more secure." +msgstr "* アカウントをより安全にするためにMFAを有効にします。" + +#: users/forms/profile.py:70 +msgid "" +"In order to protect you and your company, please keep your account, password " +"and key sensitive information properly. (for example: setting complex " +"password, enabling MFA)" +msgstr "" +"あなたとあなたの会社を保護するために、アカウント、パスワード、キーの機密情報" +"を適切に保管してください。(例: 複雑なパスワードの設定、MFAの有効化)" + +#: users/forms/profile.py:77 +msgid "Finish" +msgstr "仕上げ" + +#: users/forms/profile.py:84 +msgid "New password" +msgstr "新しいパスワード" + +#: users/forms/profile.py:89 +msgid "Confirm password" +msgstr "パスワードの確認" + +#: users/forms/profile.py:97 +msgid "Password does not match" +msgstr "パスワードが一致しない" + +#: users/forms/profile.py:118 +msgid "Old password" +msgstr "古いパスワード" + +#: users/forms/profile.py:128 +msgid "Old password error" +msgstr "古いパスワードエラー" + +#: users/forms/profile.py:138 +msgid "Automatically configure and download the SSH key" +msgstr "SSHキーの自動設定とダウンロード" + +#: users/forms/profile.py:140 +msgid "ssh public key" +msgstr "ssh公開キー" + +#: users/forms/profile.py:141 +msgid "ssh-rsa AAAA..." +msgstr "ssh-rsa AAAA.." + +#: users/forms/profile.py:142 +msgid "Paste your id_rsa.pub here." +msgstr "ここにid_rsa.pubを貼り付けます。" + +#: users/forms/profile.py:155 +msgid "Public key should not be the same as your old one." +msgstr "公開鍵は古いものと同じであってはなりません。" + +#: users/forms/profile.py:159 users/serializers/profile.py:100 +#: users/serializers/profile.py:183 users/serializers/profile.py:210 +msgid "Not a valid ssh public key" +msgstr "有効なssh公開鍵ではありません" + +#: users/forms/profile.py:170 users/models/user.py:708 +msgid "Public key" +msgstr "公開キー" + +#: users/models/user.py:561 +msgid "Force enable" +msgstr "強制有効" + +#: users/models/user.py:631 +msgid "Local" +msgstr "ローカル" + +#: users/models/user.py:687 users/serializers/user.py:160 +msgid "Is service account" +msgstr "サービスアカウントです" + +#: users/models/user.py:689 +msgid "Avatar" +msgstr "アバター" + +#: users/models/user.py:692 +msgid "Wechat" +msgstr "微信" + +#: users/models/user.py:695 +msgid "Phone" +msgstr "電話" + +#: users/models/user.py:701 +msgid "OTP secret key" +msgstr "OTP 秘密" + +#: users/models/user.py:705 +msgid "Private key" +msgstr "ssh秘密鍵" + +#: users/models/user.py:711 +msgid "Secret key" +msgstr "秘密キー" + +#: users/models/user.py:716 users/serializers/profile.py:149 +#: users/serializers/user.py:157 +msgid "Is first login" +msgstr "最初のログインです" + +#: users/models/user.py:727 +msgid "Source" +msgstr "ソース" + +#: users/models/user.py:731 +msgid "Date password last updated" +msgstr "最終更新日パスワード" + +#: users/models/user.py:734 +msgid "Need update password" +msgstr "更新パスワードが必要" + +#: users/models/user.py:902 +msgid "Can invite user" +msgstr "ユーザーを招待できます" + +#: users/models/user.py:903 +msgid "Can remove user" +msgstr "ユーザーを削除できます" + +#: users/models/user.py:904 +msgid "Can match user" +msgstr "ユーザーに一致できます" + +#: users/models/user.py:913 +msgid "Administrator" +msgstr "管理者" + +#: users/models/user.py:916 +msgid "Administrator is the super user of system" +msgstr "管理者はシステムのスーパーユーザーです" + +#: users/models/user.py:941 +msgid "User password history" +msgstr "ユーザーパスワード履歴" + +#: users/notifications.py:55 +#: users/templates/users/_msg_password_expire_reminder.html:17 +#: users/templates/users/reset_password.html:5 +#: users/templates/users/reset_password.html:6 +msgid "Reset password" +msgstr "パスワードのリセット" + +#: users/notifications.py:85 users/views/profile/reset.py:194 +msgid "Reset password success" +msgstr "パスワードのリセット成功" + +#: users/notifications.py:117 +msgid "Reset public key success" +msgstr "公開鍵のリセット成功" + +#: users/notifications.py:143 +msgid "Password is about expire" +msgstr "パスワードの有効期限が近づいています" + +#: users/notifications.py:171 +msgid "Account is about expire" +msgstr "アカウントの有効期限が近づいています" + +#: users/notifications.py:193 +msgid "Reset SSH Key" +msgstr "SSHキーのリセット" + +#: users/notifications.py:214 +msgid "Reset MFA" +msgstr "MFAのリセット" + +#: users/serializers/profile.py:30 +msgid "The old password is incorrect" +msgstr "古いパスワードが正しくありません" + +#: users/serializers/profile.py:37 users/serializers/profile.py:197 +msgid "Password does not match security rules" +msgstr "パスワードがセキュリティルールと一致しない" + +#: users/serializers/profile.py:41 +msgid "The new password cannot be the last {} passwords" +msgstr "新しいパスワードを最後の {} 個のパスワードにすることはできません" + +#: users/serializers/profile.py:49 users/serializers/profile.py:71 +msgid "The newly set password is inconsistent" +msgstr "新しく設定されたパスワードが一致しない" + +#: users/serializers/user.py:31 +msgid "System roles" +msgstr "システムの役割" + +#: users/serializers/user.py:35 +msgid "Org roles" +msgstr "組織ロール" + +#: users/serializers/user.py:84 +msgid "Password strategy" +msgstr "パスワード戦略" + +#: users/serializers/user.py:86 +msgid "MFA enabled" +msgstr "MFA有効化" + +#: users/serializers/user.py:88 +msgid "MFA force enabled" +msgstr "MFAフォース有効化" + +#: users/serializers/user.py:90 +msgid "Login blocked" +msgstr "ログインブロック" + +#: users/serializers/user.py:93 +msgid "Can public key authentication" +msgstr "公開鍵認証が可能" + +#: users/serializers/user.py:162 +msgid "Avatar url" +msgstr "アバターURL" + +#: users/serializers/user.py:165 +msgid "Is OTP bound" +msgstr "仮想MFAがバインドされているか" + +#: users/serializers/user.py:272 +msgid "Select users" +msgstr "ユーザーの選択" + +#: users/serializers/user.py:273 +msgid "For security, only list several users" +msgstr "セキュリティのために、複数のユーザーのみをリストします" + +#: users/serializers/user.py:307 +msgid "name not unique" +msgstr "名前が一意ではない" + +#: users/templates/users/_msg_account_expire_reminder.html:7 +msgid "Your account will expire in" +msgstr "アカウントの有効期限は" + +#: users/templates/users/_msg_account_expire_reminder.html:8 +msgid "" +"In order not to affect your normal work, please contact the administrator " +"for confirmation." +msgstr "" +"通常の作業に影響を与えないように、確認のために管理者に連絡してください。" + +#: users/templates/users/_msg_password_expire_reminder.html:7 +msgid "Your password will expire in" +msgstr "パスワードは" + +#: users/templates/users/_msg_password_expire_reminder.html:8 +msgid "" +"For your account security, please click on the link below to update your " +"password in time" +msgstr "" +"アカウントのセキュリティについては、下のリンクをクリックしてパスワードを時間" +"内に更新してください" + +#: users/templates/users/_msg_password_expire_reminder.html:11 +msgid "Click here update password" +msgstr "ここをクリック更新パスワード" + +#: users/templates/users/_msg_password_expire_reminder.html:16 +msgid "If your password has expired, please click the link below to" +msgstr "" +"パスワードの有効期限が切れている場合は、以下のリンクをクリックしてください" + +#: users/templates/users/_msg_reset_mfa.html:7 +msgid "Your MFA has been reset by site administrator" +msgstr "MFAはサイト管理者によってリセットされました" + +#: users/templates/users/_msg_reset_mfa.html:8 +#: users/templates/users/_msg_reset_ssh_key.html:8 +msgid "Please click the link below to set" +msgstr "以下のリンクをクリックして設定してください" + +#: users/templates/users/_msg_reset_mfa.html:11 +#: users/templates/users/_msg_reset_ssh_key.html:11 +msgid "Click here set" +msgstr "ここをクリックセット" + +#: users/templates/users/_msg_reset_ssh_key.html:7 +msgid "Your ssh public key has been reset by site administrator" +msgstr "あなたのssh公開鍵はサイト管理者によってリセットされました" + +#: users/templates/users/_msg_user_created.html:15 +msgid "click here to set your password" +msgstr "ここをクリックしてパスワードを設定してください" + +#: users/templates/users/forgot_password.html:32 +msgid "Input your email account, that will send a email to your" +msgstr "あなたのメールを入力し、それはあなたにメールを送信します" + +#: users/templates/users/forgot_password.html:35 +msgid "" +"Enter your mobile number and a verification code will be sent to your phone" +msgstr "携帯電話番号を入力すると、認証コードが携帯電話に送信されます" + +#: users/templates/users/forgot_password.html:57 +msgid "Email account" +msgstr "メールアドレス" + +#: users/templates/users/forgot_password.html:61 +msgid "Mobile number" +msgstr "携帯番号" + +#: users/templates/users/forgot_password.html:68 +msgid "Send" +msgstr "送信" + +#: users/templates/users/forgot_password.html:72 +#: users/templates/users/forgot_password_previewing.html:30 +msgid "Submit" +msgstr "送信" + +#: users/templates/users/forgot_password_previewing.html:21 +msgid "Please enter the username for which you want to retrieve the password" +msgstr "パスワードを取り戻す必要があるユーザー名を入力してください" + +#: users/templates/users/mfa_setting.html:24 +msgid "Enable MFA" +msgstr "MFAの有効化" + +#: users/templates/users/mfa_setting.html:30 +msgid "MFA force enable, cannot disable" +msgstr "MFA強制有効化、無効化できません" + +#: users/templates/users/mfa_setting.html:48 +msgid "MFA setting" +msgstr "MFAの設定" + +#: users/templates/users/reset_password.html:23 +msgid "Your password must satisfy" +msgstr "パスワードを満たす必要があります" + +#: users/templates/users/reset_password.html:24 +msgid "Password strength" +msgstr "パスワードの強さ" + +#: users/templates/users/reset_password.html:48 +msgid "Very weak" +msgstr "非常に弱い" + +#: users/templates/users/reset_password.html:49 +msgid "Weak" +msgstr "弱い" + +#: users/templates/users/reset_password.html:51 +msgid "Medium" +msgstr "中" + +#: users/templates/users/reset_password.html:52 +msgid "Strong" +msgstr "強い" + +#: users/templates/users/reset_password.html:53 +msgid "Very strong" +msgstr "非常に強い" + +#: users/templates/users/user_otp_check_password.html:6 +msgid "Enable OTP" +msgstr "OTPの有効化" + +#: users/templates/users/user_otp_enable_bind.html:6 +msgid "Bind one-time password authenticator" +msgstr "ワンタイムパスワード認証子のバインド" + +#: users/templates/users/user_otp_enable_bind.html:13 +msgid "" +"Use the MFA Authenticator application to scan the following qr code for a 6-" +"bit verification code" +msgstr "" +"MFA Authenticatorアプリケーションを使用して、次のqrコードを6ビット検証コード" +"でスキャンします。" + +#: users/templates/users/user_otp_enable_bind.html:22 +#: users/templates/users/user_verify_mfa.html:27 +msgid "Six figures" +msgstr "6つの数字" + +#: users/templates/users/user_otp_enable_install_app.html:6 +msgid "Install app" +msgstr "アプリのインストール" + +#: users/templates/users/user_otp_enable_install_app.html:13 +msgid "" +"Download and install the MFA Authenticator application on your phone or " +"applet of WeChat" +msgstr "" +"携帯電話またはWeChatのアプレットにMFA Authenticatorアプリケーションをダウン" +"ロードしてインストールします" + +#: users/templates/users/user_otp_enable_install_app.html:18 +msgid "Android downloads" +msgstr "Androidのダウンロード" + +#: users/templates/users/user_otp_enable_install_app.html:23 +msgid "iPhone downloads" +msgstr "IPhoneのダウンロード" + +#: users/templates/users/user_otp_enable_install_app.html:26 +msgid "" +"After installation, click the next step to enter the binding page (if " +"installed, go to the next step directly)." +msgstr "" +"インストール後、次のステップをクリックしてバインディングページに入ります (イ" +"ンストールされている場合は、次のステップに直接進みます)。" + +#: users/templates/users/user_password_verify.html:8 +#: users/templates/users/user_password_verify.html:9 +msgid "Verify password" +msgstr "パスワードの確認" + +#: users/templates/users/user_verify_mfa.html:9 +msgid "Authenticate" +msgstr "認証" + +#: users/templates/users/user_verify_mfa.html:15 +msgid "" +"The account protection has been opened, please complete the following " +"operations according to the prompts" +msgstr "" +"アカウント保護が開始されました。プロンプトに従って次の操作を完了してください" + +#: users/templates/users/user_verify_mfa.html:17 +msgid "Open MFA Authenticator and enter the 6-bit dynamic code" +msgstr "MFA Authenticatorを開き、6ビットの動的コードを入力します" + +#: users/views/profile/otp.py:87 +msgid "Already bound" +msgstr "すでにバインド済み" + +#: users/views/profile/otp.py:88 +msgid "MFA already bound, disable first, then bound" +msgstr "" +"MFAはすでにバインドされており、最初に無効にしてからバインドされています。" + +#: users/views/profile/otp.py:115 +msgid "OTP enable success" +msgstr "OTP有効化成功" + +#: users/views/profile/otp.py:116 +msgid "OTP enable success, return login page" +msgstr "OTP有効化成功、ログインページを返す" + +#: users/views/profile/otp.py:158 +msgid "Disable OTP" +msgstr "OTPの無効化" + +#: users/views/profile/otp.py:164 +msgid "OTP disable success" +msgstr "OTP無効化成功" + +#: users/views/profile/otp.py:165 +msgid "OTP disable success, return login page" +msgstr "OTP無効化成功、ログインページを返す" + +#: users/views/profile/password.py:36 users/views/profile/password.py:41 +msgid "Password invalid" +msgstr "パスワード無効" + +#: users/views/profile/reset.py:47 +msgid "" +"Non-local users can log in only from third-party platforms and cannot change " +"their passwords: {}" +msgstr "" +"ローカル以外のユーザーは、サードパーティ プラットフォームからのログインのみが" +"許可され、パスワードの変更はサポートされていません: {}" + +#: users/views/profile/reset.py:149 users/views/profile/reset.py:160 +msgid "Token invalid or expired" +msgstr "トークンが無効または期限切れ" + +#: users/views/profile/reset.py:165 +msgid "User auth from {}, go there change password" +msgstr "ユーザー認証ソース {}, 対応するシステムにパスワードを変更してください" + +#: users/views/profile/reset.py:172 +msgid "* Your password does not meet the requirements" +msgstr "* パスワードが要件を満たしていない" + +#: users/views/profile/reset.py:178 +msgid "* The new password cannot be the last {} passwords" +msgstr "* 新しいパスワードを最後の {} パスワードにすることはできません" + +#: users/views/profile/reset.py:195 +msgid "Reset password success, return to login page" +msgstr "パスワードの成功をリセットし、ログインページに戻る" + +#: xpack/apps.py:8 +msgid "XPACK" +msgstr "XPack" + +#: xpack/plugins/cloud/api.py:40 +msgid "Test connection successful" +msgstr "テスト接続成功" + +#: xpack/plugins/cloud/api.py:42 +msgid "Test connection failed: {}" +msgstr "テスト接続に失敗しました: {}" + +#: xpack/plugins/cloud/const.py:8 +msgid "Alibaba Cloud" +msgstr "アリ雲" + +#: xpack/plugins/cloud/const.py:9 +msgid "AWS (International)" +msgstr "AWS (国際)" + +#: xpack/plugins/cloud/const.py:10 +msgid "AWS (China)" +msgstr "AWS (中国)" + +#: xpack/plugins/cloud/const.py:11 +msgid "Azure (China)" +msgstr "Azure (中国)" + +#: xpack/plugins/cloud/const.py:12 +msgid "Azure (International)" +msgstr "Azure (国際)" + +#: xpack/plugins/cloud/const.py:14 +msgid "Baidu Cloud" +msgstr "百度雲" + +#: xpack/plugins/cloud/const.py:15 +msgid "JD Cloud" +msgstr "京東雲" + +#: xpack/plugins/cloud/const.py:16 +msgid "KingSoft Cloud" +msgstr "金山雲" + +#: xpack/plugins/cloud/const.py:17 +msgid "Tencent Cloud" +msgstr "テンセント雲" + +#: xpack/plugins/cloud/const.py:18 +msgid "Tencent Cloud (Lighthouse)" +msgstr "テンセント雲(軽量アプリケーション)" + +#: xpack/plugins/cloud/const.py:19 +msgid "VMware" +msgstr "VMware" + +#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 +msgid "Nutanix" +msgstr "Nutanix" + +#: xpack/plugins/cloud/const.py:21 +msgid "Huawei Private Cloud" +msgstr "華為私有雲" + +#: xpack/plugins/cloud/const.py:22 +msgid "Qingyun Private Cloud" +msgstr "青雲私有雲" + +#: xpack/plugins/cloud/const.py:23 +msgid "CTYun Private Cloud" +msgstr "スカイウィング私有雲" + +#: xpack/plugins/cloud/const.py:24 +msgid "OpenStack" +msgstr "OpenStack" + +#: xpack/plugins/cloud/const.py:25 +msgid "Google Cloud Platform" +msgstr "谷歌雲" + +#: xpack/plugins/cloud/const.py:26 +msgid "Fusion Compute" +msgstr "" + +#: xpack/plugins/cloud/const.py:31 +msgid "Private IP" +msgstr "プライベートIP" + +#: xpack/plugins/cloud/const.py:32 +msgid "Public IP" +msgstr "パブリックIP" + +#: xpack/plugins/cloud/const.py:36 +msgid "Instance name" +msgstr "インスタンス名" + +#: xpack/plugins/cloud/const.py:37 +msgid "Instance name and Partial IP" +msgstr "インスタンス名と部分IP" + +#: xpack/plugins/cloud/const.py:42 +msgid "Succeed" +msgstr "成功" + +#: xpack/plugins/cloud/const.py:46 +msgid "Unsync" +msgstr "同期していません" + +#: xpack/plugins/cloud/const.py:47 +msgid "New Sync" +msgstr "新しい同期" + +#: xpack/plugins/cloud/const.py:48 +msgid "Synced" +msgstr "同期済み" + +#: xpack/plugins/cloud/const.py:49 +msgid "Released" +msgstr "リリース済み" + +#: xpack/plugins/cloud/meta.py:9 +msgid "Cloud center" +msgstr "クラウドセンター" + +#: xpack/plugins/cloud/models.py:30 +msgid "Provider" +msgstr "プロバイダー" + +#: xpack/plugins/cloud/models.py:34 +msgid "Validity" +msgstr "有効性" + +#: xpack/plugins/cloud/models.py:39 +msgid "Cloud account" +msgstr "クラウドアカウント" + +#: xpack/plugins/cloud/models.py:41 +msgid "Test cloud account" +msgstr "クラウドアカウントのテスト" + +#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37 +msgid "Regions" +msgstr "リージョン" + +#: xpack/plugins/cloud/models.py:91 +msgid "Hostname strategy" +msgstr "ホスト名戦略" + +#: xpack/plugins/cloud/models.py:100 +msgid "Unix admin user" +msgstr "Unix adminユーザー" + +#: xpack/plugins/cloud/models.py:104 +msgid "Windows admin user" +msgstr "Windows管理者" + +#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:41 +msgid "IP network segment group" +msgstr "IPネットワークセグメントグループ" + +#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:46 +msgid "Sync IP type" +msgstr "同期IPタイプ" + +#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/serializers/task.py:64 +msgid "Always update" +msgstr "常に更新" + +#: xpack/plugins/cloud/models.py:122 +msgid "Date last sync" +msgstr "最終同期日" + +#: xpack/plugins/cloud/models.py:127 xpack/plugins/cloud/models.py:168 +msgid "Sync instance task" +msgstr "インスタンスの同期タスク" + +#: xpack/plugins/cloud/models.py:179 xpack/plugins/cloud/models.py:227 +msgid "Date sync" +msgstr "日付の同期" + +#: xpack/plugins/cloud/models.py:183 +msgid "Sync instance task execution" +msgstr "インスタンスタスクの同期実行" + +#: xpack/plugins/cloud/models.py:207 +msgid "Sync task" +msgstr "同期タスク" + +#: xpack/plugins/cloud/models.py:211 +msgid "Sync instance task history" +msgstr "インスタンスタスク履歴の同期" + +#: xpack/plugins/cloud/models.py:214 +msgid "Instance" +msgstr "インスタンス" + +#: xpack/plugins/cloud/models.py:231 +msgid "Sync instance detail" +msgstr "同期インスタンスの詳細" + +#: xpack/plugins/cloud/providers/aws_international.py:17 +msgid "China (Beijing)" +msgstr "中国 (北京)" + +#: xpack/plugins/cloud/providers/aws_international.py:18 +msgid "China (Ningxia)" +msgstr "中国 (寧夏)" + +#: xpack/plugins/cloud/providers/aws_international.py:21 +msgid "US East (Ohio)" +msgstr "米国東部 (オハイオ州)" + +#: xpack/plugins/cloud/providers/aws_international.py:22 +msgid "US East (N. Virginia)" +msgstr "米国東部 (N. バージニア州)" + +#: xpack/plugins/cloud/providers/aws_international.py:23 +msgid "US West (N. California)" +msgstr "米国西部 (N. カリフォルニア州)" + +#: xpack/plugins/cloud/providers/aws_international.py:24 +msgid "US West (Oregon)" +msgstr "米国西部 (オレゴン州)" + +#: xpack/plugins/cloud/providers/aws_international.py:25 +msgid "Africa (Cape Town)" +msgstr "アフリカ (ケープタウン)" + +#: xpack/plugins/cloud/providers/aws_international.py:26 +msgid "Asia Pacific (Hong Kong)" +msgstr "アジアパシフィック (香港)" + +#: xpack/plugins/cloud/providers/aws_international.py:27 +msgid "Asia Pacific (Mumbai)" +msgstr "アジア太平洋 (ムンバイ)" + +#: xpack/plugins/cloud/providers/aws_international.py:28 +msgid "Asia Pacific (Osaka-Local)" +msgstr "アジアパシフィック (大阪-ローカル)" + +#: xpack/plugins/cloud/providers/aws_international.py:29 +msgid "Asia Pacific (Seoul)" +msgstr "アジア太平洋地域 (ソウル)" + +#: xpack/plugins/cloud/providers/aws_international.py:30 +msgid "Asia Pacific (Singapore)" +msgstr "アジア太平洋 (シンガポール)" + +#: xpack/plugins/cloud/providers/aws_international.py:31 +msgid "Asia Pacific (Sydney)" +msgstr "アジア太平洋 (シドニー)" + +#: xpack/plugins/cloud/providers/aws_international.py:32 +msgid "Asia Pacific (Tokyo)" +msgstr "アジアパシフィック (東京)" + +#: xpack/plugins/cloud/providers/aws_international.py:33 +msgid "Canada (Central)" +msgstr "カナダ (中央)" + +#: xpack/plugins/cloud/providers/aws_international.py:34 +msgid "Europe (Frankfurt)" +msgstr "ヨーロッパ (フランクフルト)" + +#: xpack/plugins/cloud/providers/aws_international.py:35 +msgid "Europe (Ireland)" +msgstr "ヨーロッパ (アイルランド)" + +#: xpack/plugins/cloud/providers/aws_international.py:36 +msgid "Europe (London)" +msgstr "ヨーロッパ (ロンドン)" + +#: xpack/plugins/cloud/providers/aws_international.py:37 +msgid "Europe (Milan)" +msgstr "ヨーロッパ (ミラノ)" + +#: xpack/plugins/cloud/providers/aws_international.py:38 +msgid "Europe (Paris)" +msgstr "ヨーロッパ (パリ)" + +#: xpack/plugins/cloud/providers/aws_international.py:39 +msgid "Europe (Stockholm)" +msgstr "ヨーロッパ (ストックホルム)" + +#: xpack/plugins/cloud/providers/aws_international.py:40 +msgid "Middle East (Bahrain)" +msgstr "中东 (バーレーン)" + +#: xpack/plugins/cloud/providers/aws_international.py:41 +msgid "South America (São Paulo)" +msgstr "南米 (サンパウロ)" + +#: xpack/plugins/cloud/providers/baiducloud.py:54 +#: xpack/plugins/cloud/providers/jdcloud.py:127 +msgid "CN North-Beijing" +msgstr "華北-北京" + +#: xpack/plugins/cloud/providers/baiducloud.py:55 +#: xpack/plugins/cloud/providers/huaweicloud.py:40 +#: xpack/plugins/cloud/providers/jdcloud.py:130 +msgid "CN South-Guangzhou" +msgstr "華南-広州" + +#: xpack/plugins/cloud/providers/baiducloud.py:56 +msgid "CN East-Suzhou" +msgstr "華東-蘇州" + +#: xpack/plugins/cloud/providers/baiducloud.py:57 +#: xpack/plugins/cloud/providers/huaweicloud.py:48 +msgid "CN-Hong Kong" +msgstr "中国-香港" + +#: xpack/plugins/cloud/providers/baiducloud.py:58 +msgid "CN Center-Wuhan" +msgstr "華中-武漢" + +#: xpack/plugins/cloud/providers/baiducloud.py:59 +msgid "CN North-Baoding" +msgstr "華北-保定" + +#: xpack/plugins/cloud/providers/baiducloud.py:60 +#: xpack/plugins/cloud/providers/jdcloud.py:129 +msgid "CN East-Shanghai" +msgstr "華東-上海" + +#: xpack/plugins/cloud/providers/baiducloud.py:61 +#: xpack/plugins/cloud/providers/huaweicloud.py:47 +msgid "AP-Singapore" +msgstr "アジア太平洋-シンガポール" + +#: xpack/plugins/cloud/providers/huaweicloud.py:35 +msgid "AF-Johannesburg" +msgstr "アフリカ-ヨハネスブルク" + +#: xpack/plugins/cloud/providers/huaweicloud.py:36 +msgid "CN North-Beijing4" +msgstr "華北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:37 +msgid "CN North-Beijing1" +msgstr "華北-北京1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:38 +msgid "CN East-Shanghai2" +msgstr "華東-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:39 +msgid "CN East-Shanghai1" +msgstr "華東-上海1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:41 +msgid "LA-Mexico City1" +msgstr "LA-メキシコCity1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:42 +msgid "LA-Santiago" +msgstr "ラテンアメリカ-サンディエゴ" + +#: xpack/plugins/cloud/providers/huaweicloud.py:43 +msgid "LA-Sao Paulo1" +msgstr "ラミー・サンパウロ1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:44 +msgid "EU-Paris" +msgstr "ヨーロッパ-パリ" + +#: xpack/plugins/cloud/providers/huaweicloud.py:45 +msgid "CN Southwest-Guiyang1" +msgstr "南西-貴陽1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:46 +msgid "AP-Bangkok" +msgstr "アジア太平洋-バンコク" + +#: xpack/plugins/cloud/providers/huaweicloud.py:50 +msgid "CN Northeast-Dalian" +msgstr "华北-大连" + +#: xpack/plugins/cloud/providers/huaweicloud.py:51 +msgid "CN North-Ulanqab1" +msgstr "華北-ウランチャブ一" + +#: xpack/plugins/cloud/providers/huaweicloud.py:52 +msgid "CN South-Guangzhou-InvitationOnly" +msgstr "華南-広州-友好ユーザー環境" + +#: xpack/plugins/cloud/providers/jdcloud.py:128 +msgid "CN East-Suqian" +msgstr "華東-宿遷" + +#: xpack/plugins/cloud/serializers/account.py:63 +msgid "Validity display" +msgstr "有効表示" + +#: xpack/plugins/cloud/serializers/account.py:64 +msgid "Provider display" +msgstr "プロバイダ表示" + +#: xpack/plugins/cloud/serializers/account_attrs.py:35 +msgid "Client ID" +msgstr "クライアントID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:41 +msgid "Tenant ID" +msgstr "テナントID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:44 +msgid "Subscription ID" +msgstr "サブスクリプションID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:95 +#: xpack/plugins/cloud/serializers/account_attrs.py:100 +#: xpack/plugins/cloud/serializers/account_attrs.py:116 +#: xpack/plugins/cloud/serializers/account_attrs.py:141 +msgid "API Endpoint" +msgstr "APIエンドポイント" + +#: xpack/plugins/cloud/serializers/account_attrs.py:106 +msgid "Auth url" +msgstr "認証アドレス" + +#: xpack/plugins/cloud/serializers/account_attrs.py:107 +msgid "eg: http://openstack.example.com:5000/v3" +msgstr "例えば: http://openstack.example.com:5000/v3" + +#: xpack/plugins/cloud/serializers/account_attrs.py:110 +msgid "User domain" +msgstr "ユーザードメイン" + +#: xpack/plugins/cloud/serializers/account_attrs.py:117 +msgid "Cert File" +msgstr "証明書ファイル" + +#: xpack/plugins/cloud/serializers/account_attrs.py:118 +msgid "Key File" +msgstr "キーファイル" + +#: xpack/plugins/cloud/serializers/account_attrs.py:134 +msgid "Service account key" +msgstr "サービスアカウントキー" + +#: xpack/plugins/cloud/serializers/account_attrs.py:135 +msgid "The file is in JSON format" +msgstr "ファイルはJSON形式です。" + +#: xpack/plugins/cloud/serializers/account_attrs.py:148 +msgid "IP address invalid `{}`, {}" +msgstr "IPアドレスが無効: '{}', {}" + +#: xpack/plugins/cloud/serializers/account_attrs.py:154 +msgid "" +"Format for comma-delimited string,Such as: 192.168.1.0/24, " +"10.0.0.0-10.0.0.255" +msgstr "形式はコンマ区切りの文字列です,例:192.168.1.0/24,10.0.0.0-10.0.0.255" + +#: xpack/plugins/cloud/serializers/account_attrs.py:158 +msgid "" +"The port is used to detect the validity of the IP address. When the " +"synchronization task is executed, only the valid IP address will be " +"synchronized.
If the port is 0, all IP addresses are valid." +msgstr "" +"このポートは、 IP アドレスの有効性を検出するために使用されます。同期タスクが" +"実行されると、有効な IP アドレスのみが同期されます。
ポートが0の場合、す" +"べてのIPアドレスが有効です。" + +#: xpack/plugins/cloud/serializers/account_attrs.py:166 +msgid "Hostname prefix" +msgstr "ホスト名プレフィックス" + +#: xpack/plugins/cloud/serializers/account_attrs.py:169 +msgid "IP segment" +msgstr "IP セグメント" + +#: xpack/plugins/cloud/serializers/account_attrs.py:173 +msgid "Test port" +msgstr "テストポート" + +#: xpack/plugins/cloud/serializers/account_attrs.py:176 +msgid "Test timeout" +msgstr "テストタイムアウト" + +#: xpack/plugins/cloud/serializers/task.py:28 +msgid "" +"Only instances matching the IP range will be synced.
If the instance " +"contains multiple IP addresses, the first IP address that matches will be " +"used as the IP for the created asset.
The default value of * means sync " +"all instances and randomly match IP addresses.
Format for comma-" +"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20" +msgstr "" +"IP範囲に一致するインスタンスのみが同期されます。
インスタンスに複数のIPア" +"ドレスが含まれている場合、一致する最初のIPアドレスが作成されたアセットのIPと" +"して使用されます。
デフォルト値の*は、すべてのインスタンスを同期し、IPア" +"ドレスをランダムに一致させることを意味します。
形式はコンマ区切りの文字列" +"です。例:192.168.1.0/24,10.1.1.1-10.1.1.20" + +#: xpack/plugins/cloud/serializers/task.py:35 +msgid "History count" +msgstr "実行回数" + +#: xpack/plugins/cloud/serializers/task.py:36 +msgid "Instance count" +msgstr "インスタンス数" + +#: xpack/plugins/cloud/serializers/task.py:63 +msgid "Linux admin user" +msgstr "Linux管理者" + +#: xpack/plugins/cloud/utils.py:69 +msgid "Account unavailable" +msgstr "利用できないアカウント" + +#: xpack/plugins/interface/api.py:52 +msgid "Restore default successfully." +msgstr "デフォルトの復元に成功しました。" + +#: xpack/plugins/interface/meta.py:10 +msgid "Interface settings" +msgstr "インターフェイスの設定" + +#: xpack/plugins/interface/models.py:22 +msgid "Title of login page" +msgstr "ログインページのタイトル" + +#: xpack/plugins/interface/models.py:26 +msgid "Image of login page" +msgstr "ログインページのイメージ" + +#: xpack/plugins/interface/models.py:30 +msgid "Website icon" +msgstr "ウェブサイトのアイコン" + +#: xpack/plugins/interface/models.py:34 +msgid "Logo of management page" +msgstr "管理ページのロゴ" + +#: xpack/plugins/interface/models.py:38 +msgid "Logo of logout page" +msgstr "ログアウトページのロゴ" + +#: xpack/plugins/interface/models.py:40 +msgid "Theme" +msgstr "テーマ" + +#: xpack/plugins/interface/models.py:43 xpack/plugins/interface/models.py:84 +msgid "Interface setting" +msgstr "インターフェイスの設定" + +#: xpack/plugins/license/api.py:50 +msgid "License import successfully" +msgstr "ライセンスのインポートに成功" + +#: xpack/plugins/license/api.py:51 +msgid "License is invalid" +msgstr "ライセンスが無効です" + +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +msgid "License" +msgstr "ライセンス" + +#: xpack/plugins/license/models.py:71 +msgid "Standard edition" +msgstr "標準版" + +#: xpack/plugins/license/models.py:73 +msgid "Enterprise edition" +msgstr "エンタープライズ版" + +#: xpack/plugins/license/models.py:75 +msgid "Ultimate edition" +msgstr "究極のエディション" + +#: xpack/plugins/license/models.py:77 +msgid "Community edition" +msgstr "コミュニティ版" + +#~ msgid "Welcome back, please enter username and password to login" +#~ msgstr "" +#~ "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" + +#, fuzzy +#~ msgid "Run account" +#~ msgstr "アカウント" + +#~ msgid "Category display" +#~ msgstr "カテゴリ表示" + +#, fuzzy +#~ msgid "Auto fill" +#~ msgstr "自動" + +#~ msgid "Built-in" +#~ msgstr "内蔵" + +#~ msgid "Scope display" +#~ msgstr "スコープ表示" + +#~ msgid "App change auth" +#~ msgstr "応用改密" + +#~ msgid "My apps" +#~ msgstr "マイアプリ" + +#~ msgid "Risk level display" +#~ msgstr "リスクレベル表示" + +#~ msgid "From cmd filter" +#~ msgstr "コマンドフィルタ規則から" + +#~ msgid "From cmd filter rule" +#~ msgstr "コマンドフィルタ規則から" + +#~ msgid "System roles display" +#~ msgstr "システムロール表示" + +#~ msgid "Org roles display" +#~ msgstr "組織ロール表示" + +#~ msgid "MFA level display" +#~ msgstr "MFAレベル表示" + +#~ msgid "Groups name" +#~ msgstr "グループ名" + +#~ msgid "Source name" +#~ msgstr "ソース名" + +#~ msgid "Organization role name" +#~ msgstr "組織の役割名" + +#~ msgid "Super role name" +#~ msgstr "スーパーロール名" + +#~ msgid "Total role name" +#~ msgstr "合計ロール名" + +#~ msgid "Is wecom bound" +#~ msgstr "企業の微信をバインドしているかどうか" + +#~ msgid "Is dingtalk bound" +#~ msgstr "ピンをバインドしているかどうか" + +#~ msgid "Is feishu bound" +#~ msgstr "飛本を縛ったかどうか" + +#~ msgid "System role name" +#~ msgstr "システムロール名" + +#~ msgid "Change auth plan" +#~ msgstr "密かな計画" + +#~ msgid "Application change auth plan" +#~ msgstr "改密計画の適用" + +#~ msgid "Application change auth plan execution" +#~ msgstr "改密計画実行の適用" + +#~ msgid "App" +#~ msgstr "適用" + +#~ msgid "Application change auth plan task" +#~ msgstr "改密計画タスクの適用" + +#~ msgid "Password cannot be set to blank, exit. " +#~ msgstr "パスワードを空白に設定することはできません。" + +#~ msgid "Asset change auth plan" +#~ msgstr "資産変更のオースプラン" + +#~ msgid "Asset change auth plan execution" +#~ msgstr "資産変更のオースプランの実行" + +#~ msgid "Change auth plan execution" +#~ msgstr "改密計画の実行" + +#~ msgid "Asset change auth plan task" +#~ msgstr "資産改密計画タスク" + +#~ msgid "This asset does not have a privileged user set: " +#~ msgstr "このアセットには特権ユーザーセットがありません。" + +#~ msgid "" +#~ "The password and key of the current asset privileged user cannot be " +#~ "changed: " +#~ msgstr "現在のアセット特権ユーザーのパスワードとキーは変更できません。" + +#~ msgid "Public key cannot be set to null, exit. " +#~ msgstr "公開鍵をnull、exitに設定することはできません。" + +#~ msgid "Change auth plan snapshot" +#~ msgstr "計画スナップショットの暗号化" + +#~ msgid "Preflight check" +#~ msgstr "プリフライトチェック" + +#~ msgid "Change auth" +#~ msgstr "秘密を改める" + +#~ msgid "Verify auth" +#~ msgstr "パスワード/キーの確認" + +#~ msgid "Keep auth" +#~ msgstr "パスワード/キーの保存" + +#~ msgid "Step" +#~ msgstr "ステップ" + +#~ msgid "Change Password" +#~ msgstr "パスワードの変更" + +#~ msgid "Change SSH Key" +#~ msgstr "SSHキーの変更" + +#~ msgid "Run times" +#~ msgstr "実行時間" + +#~ msgid "After many attempts to change the secret, it still failed" +#~ msgstr "秘密を変更しようとする多くの試みの後、それはまだ失敗しました" + +#~ msgid "Invalid/incorrect password" +#~ msgstr "パスワードが無効/間違っている" + +#~ msgid "Failed to connect to the host" +#~ msgstr "ホストへの接続に失敗しました" + +#~ msgid "Data could not be sent to remote" +#~ msgstr "データをリモートに送信できませんでした" + +#~ msgid "Periodic display" +#~ msgstr "定期的な表示" + +#~ msgid "Gathered user" +#~ msgstr "収集されたユーザー" + +#~ msgid "Gather user task" +#~ msgstr "ユーザータスクの収集" + +#~ msgid "Assets is empty, please change nodes" +#~ msgstr "資産は空です。ノードを変更してください" + +#~ msgid "Executed times" +#~ msgstr "実行時間" + +#~ msgid "System User" +#~ msgstr "システムユーザー" + +#~ msgid "" +#~ "Format for comma-delimited string, with * indicating a match all. " +#~ "Protocol options: {}" +#~ msgstr "" +#~ "コンマ区切り文字列の形式。* はすべて一致することを示します。プロトコルオプ" +#~ "ション: {}" + +#~ msgid "Unsupported protocols: {}" +#~ msgstr "サポートされていないプロトコル: {}" + +#~ msgid "Remote app" +#~ msgstr "リモートアプリ" + +#~ msgid "Custom" +#~ msgstr "カスタム" + +#~ msgid "Can view application account secret" +#~ msgstr "アプリケーションアカウントの秘密を表示できます" + +#~ msgid "Can change application account secret" +#~ msgstr "アプリケーションアカウントの秘密を変更できます" + +#~ msgid "Application user" +#~ msgstr "アプリケーションユーザー" + +#~ msgid "Type display" +#~ msgstr "タイプ表示" + +#~ msgid "Application display" +#~ msgstr "アプリケーション表示" + +#~ msgid "Cluster" +#~ msgstr "クラスター" + +#~ msgid "CA certificate" +#~ msgstr "CA 証明書" + +#~ msgid "Client certificate file" +#~ msgstr "クライアント証明書" + +#~ msgid "Certificate key file" +#~ msgstr "証明書キー" + +#~ msgid "Application path" +#~ msgstr "アプリケーションパス" + +#~ msgid "Target URL" +#~ msgstr "ターゲットURL" + +#~ msgid "Chrome username" +#~ msgstr "Chromeユーザー名" + +#~ msgid "Chrome password" +#~ msgstr "Chromeパスワード" + +#~ msgid "Operating parameter" +#~ msgstr "操作パラメータ" + +#~ msgid "Target url" +#~ msgstr "ターゲットURL" + +#~ msgid "Mysql workbench username" +#~ msgstr "Mysql workbench のユーザー名" + +#~ msgid "Mysql workbench password" +#~ msgstr "Mysql workbench パスワード" + +#~ msgid "Vmware username" +#~ msgstr "Vmware ユーザー名" + +#~ msgid "Vmware password" +#~ msgstr "Vmware パスワード" + +#~ msgid "Base" +#~ msgstr "ベース" + +#~ msgid "Can test asset account connectivity" +#~ msgstr "アセットアカウントの接続性をテストできます" + +#~ msgid "Bandwidth" +#~ msgstr "帯域幅" + +#~ msgid "Contact" +#~ msgstr "連絡先" + +#~ msgid "Intranet" +#~ msgstr "イントラネット" + +#~ msgid "Extranet" +#~ msgstr "エクストラネット" + +#~ msgid "Operator" +#~ msgstr "オペレーター" + +#~ msgid "Default Cluster" +#~ msgstr "デフォルトクラスター" + +#~ msgid "Test gateway" +#~ msgstr "テストゲートウェイ" + +#~ msgid "User groups" +#~ msgstr "ユーザーグループ" + +#~ msgid "System user display" +#~ msgstr "システムユーザー表示" + +#~ msgid "Protocol format should {}/{}" +#~ msgstr "プロトコル形式は {}/{}" + +#~ msgid "Nodes name" +#~ msgstr "ノード名" + +#~ msgid "Labels name" +#~ msgstr "ラベル名" + +#~ msgid "Hardware info" +#~ msgstr "ハードウェア情報" + +#~ msgid "Admin user display" +#~ msgstr "管理者ユーザー表示" + +#~ msgid "CPU info" +#~ msgstr "CPU情報" + +#~ msgid "Action display" +#~ msgstr "アクション表示" + +#~ msgid "Applications amount" +#~ msgstr "申し込み金額" + +#~ msgid "Gateways count" +#~ msgstr "ゲートウェイ数" + +#~ msgid "SSH key fingerprint" +#~ msgstr "SSHキー指紋" + +#~ msgid "Apps amount" +#~ msgstr "アプリの量" + +#~ msgid "Login mode display" +#~ msgstr "ログインモード表示" + +#~ msgid "Ad domain" +#~ msgstr "広告ドメイン" + +#~ msgid "Is asset protocol" +#~ msgstr "資産プロトコルです" + +#~ msgid "Only ssh and automatic login system users are supported" +#~ msgstr "sshと自動ログインシステムのユーザーのみがサポートされています" + +#~ msgid "Username same with user with protocol {} only allow 1" +#~ msgstr "プロトコル {} のユーザーと同じユーザー名は1のみ許可します" + +#~ msgid "* Automatic login mode must fill in the username." +#~ msgstr "* 自動ログインモードはユーザー名を入力する必要があります。" + +#~ msgid "Path should starts with /" +#~ msgstr "パスは/で始まる必要があります" + +#~ msgid "Password or private key required" +#~ msgstr "パスワードまたは秘密鍵が必要" + +#~ msgid "Only ssh protocol system users are allowed" +#~ msgstr "Sshプロトコルシステムユーザーのみが許可されています" + +#~ msgid "The protocol must be consistent with the current user: {}" +#~ msgstr "プロトコルは現在のユーザーと一致している必要があります: {}" + +#~ msgid "Only system users with automatic login are allowed" +#~ msgstr "自動ログインを持つシステムユーザーのみが許可されます" + +#~ msgid "System user name" +#~ msgstr "システムユーザー名" + +#~ msgid "Asset hostname" +#~ msgstr "資産ホスト名" + +#~ msgid "The asset {} system platform {} does not support run Ansible tasks" +#~ msgstr "" +#~ "資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしてい" +#~ "ません。" + +#~ msgid "Test assets connectivity: " +#~ msgstr "資産の接続性のテスト:" + +#~ msgid "Unreachable" +#~ msgstr "達成できない" + +#~ msgid "Reachable" +#~ msgstr "接続可能" + +#~ msgid "Get asset info failed: {}" +#~ msgstr "資産情報の取得に失敗しました: {}" + +#~ msgid "Update asset hardware info: " +#~ msgstr "資産ハードウェア情報の更新:" + +#~ msgid "System user is dynamic: {}" +#~ msgstr "システムユーザーは動的です: {}" + +#~ msgid "Start push system user for platform: [{}]" +#~ msgstr "プラットフォームのプッシュシステムユーザーを開始: [{}]" + +#~ msgid "Hosts count: {}" +#~ msgstr "ホスト数: {}" + +#~ msgid "Push system users to asset: " +#~ msgstr "システムユーザーをアセットにプッシュする:" + +#~ msgid "Dynamic system user not support test" +#~ msgstr "動的システムユーザーがテストをサポートしていない" + +#~ msgid "Start test system user connectivity for platform: [{}]" +#~ msgstr "プラットフォームのテストシステムのユーザー接続を開始: [{}]" + +#~ msgid "Test system user connectivity: " +#~ msgstr "テストシステムユーザー接続:" + +#~ msgid "Test system user connectivity period: " +#~ msgstr "テストシステムユーザー接続期间:" + +#~ msgid "Operate display" +#~ msgstr "ディスプレイを操作する" + +#~ msgid "Status display" +#~ msgstr "ステータス表示" + +#~ msgid "MFA display" +#~ msgstr "MFAディスプレイ" + +#~ msgid "Hosts display" +#~ msgstr "ホスト表示" + +#~ msgid "Run as" +#~ msgstr "として実行" + +#~ msgid "Run as display" +#~ msgstr "ディスプレイとして実行する" + +#~ msgid "User not exists" +#~ msgstr "ユーザーは存在しません" + +#~ msgid "System user not exists" +#~ msgstr "システムユーザーが存在しません" + +#~ msgid "Asset not exists" +#~ msgstr "アセットが存在しません" + +#~ msgid "User has no permission to access asset or permission expired" +#~ msgstr "" +#~ "ユーザーがアセットにアクセスする権限を持っていないか、権限の有効期限が切れ" +#~ "ています" + +#~ msgid "User has no permission to access application or permission expired" +#~ msgstr "" +#~ "ユーザーがアプリにアクセスする権限を持っていないか、権限の有効期限が切れて" +#~ "います" + +#~ msgid "Asset or application required" +#~ msgstr "アセットまたはアプリが必要" + +#~ msgid "Not has host {} permission" +#~ msgstr "ホスト {} 権限がありません" + +#~ msgid "" +#~ "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " +#~ "crontab expressions (Online tools)
Note: If both Regularly " +#~ "perform and Cycle perform are set, give priority to Regularly perform" +#~ msgstr "" +#~ "eg:毎週日03:05<5 3**0>
ヒント:5ビットLinux crontab式<分時日月曜日>(オンラインワーク)
" +#~ "注意:定期実行と周期実行を同時に設定した場合は、定期実行を優先します。" + +#~ msgid "Unit: hour" +#~ msgstr "単位: 時間" + +#~ msgid "Callback" +#~ msgstr "コールバック" + +#~ msgid "Can view task monitor" +#~ msgstr "タスクモニターを表示できます" + +#~ msgid "Tasks" +#~ msgstr "タスク" + +#~ msgid "Run as admin" +#~ msgstr "再実行" + +#~ msgid "Become" +#~ msgstr "になる" + +#~ msgid "Create by" +#~ msgstr "による作成" + +#~ msgid "Task display" +#~ msgstr "タスク表示" + +#~ msgid "Host amount" +#~ msgstr "ホスト量" + +#~ msgid "Start time" +#~ msgstr "開始時間" + +#~ msgid "End time" +#~ msgstr "終了時間" + +#~ msgid "Adhoc raw result" +#~ msgstr "アドホック生の結果" + +#~ msgid "Adhoc result summary" +#~ msgstr "アドホック結果の概要" + +#~ msgid "Task start" +#~ msgstr "タスクの開始" + +#~ msgid "Command `{}` is forbidden ........" +#~ msgstr "コマンド '{}' は禁止されています ........" + +#~ msgid "Clean task history period" +#~ msgstr "クリーンなタスク履歴期間" + +#~ msgid "The administrator is modifying permissions. Please wait" +#~ msgstr "管理者は権限を変更しています。お待ちください" + +#~ msgid "The authorization cannot be revoked for the time being" +#~ msgstr "当分の間、承認を取り消すことはできません。" + +#~ msgid "Application permission" +#~ msgstr "申請許可" + +#~ msgid "Permed application" +#~ msgstr "許可されたアプリケーション" + +#~ msgid "Can view my apps" +#~ msgstr "自分のアプリを表示できます" + +#~ msgid "Can view user apps" +#~ msgstr "ユーザーアプリを表示できます" + +#~ msgid "Can view usergroup apps" +#~ msgstr "ユーザー・グループ認可の適用を表示できます" + +#~ msgid "Upload file" +#~ msgstr "ファイルのアップロード" + +#~ msgid "Download file" +#~ msgstr "ファイルのダウンロード" + +#~ msgid "Upload download" +#~ msgstr "ダウンロードのアップロード" + +#~ msgid "Clipboard paste" +#~ msgstr "クリップボードペースト" + +#~ msgid "Clipboard copy paste" +#~ msgstr "クリップボードコピーペースト" + +#~ msgid "Your permed applications is about to expire" +#~ msgstr "パーマアプリケーションの有効期限が近づいています" + +#~ msgid "permed applications" +#~ msgstr "Permedアプリケーション" + +#~ msgid "Application permissions is about to expire" +#~ msgstr "アプリケーション権限の有効期限が近づいています" + +#~ msgid "application permissions of organization {}" +#~ msgstr "Organization {} のアプリケーション権限" + +#~ msgid "User groups amount" +#~ msgstr "ユーザーグループの量" + +#~ msgid "System users amount" +#~ msgstr "システムユーザー数" + +#~ msgid "" +#~ "The application list contains applications that are different from the " +#~ "permission type. ({})" +#~ msgstr "" +#~ "アプリケーションリストには、権限タイプとは異なるアプリケーションが含まれて" +#~ "います。({})" + +#~ msgid "Users display" +#~ msgstr "ユーザー表示" + +#~ msgid "User groups display" +#~ msgstr "ユーザーグループの表示" + +#~ msgid "Assets display" +#~ msgstr "資産表示" + +#~ msgid "Nodes display" +#~ msgstr "ノード表示" + +#~ msgid "System users display" +#~ msgstr "システムユーザーの表示" + +#~ msgid "My applications" +#~ msgstr "私のアプリケーション" + +#~ msgid "Empty" +#~ msgstr "空" + +#~ msgid "System user ID" +#~ msgstr "システムユーザーID" + +#~ msgid "Apply for application" +#~ msgstr "申し込み" + +#~ msgid "" +#~ "Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " +#~ "processor: {}, ticket ID: {}" +#~ msgstr "" +#~ "チケットによって作成されたチケットタイトル: {}、チケット申請者: {}、チケッ" +#~ "ト処理者: {}、チケットID: {}" + +#~ msgid "Applied login IP" +#~ msgstr "応用ログインIP" + +#~ msgid "Applied login city" +#~ msgstr "応用ログイン都市" + +#~ msgid "Applied login datetime" +#~ msgstr "適用されたログインの日付時間" + +#~ msgid "Login system user" +#~ msgstr "ログインシステムユーザー" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 0ff05dd98..f75598dd1 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c09abdddb5699aeaf832e1162b58ea9b520c10df3f80390c0ec680da3e18f4d -size 103641 +oid sha256:c73f2cbae215e4169dd5d71cb12c434caf465f2d81e37bb90aa2305cebb3ab39 +size 105850 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po new file mode 100644 index 000000000..c7ad08aa0 --- /dev/null +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -0,0 +1,7511 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: JumpServer 0.3.3\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-12-27 12:11+0800\n" +"PO-Revision-Date: 2021-05-20 10:54+0800\n" +"Last-Translator: ibuler \n" +"Language-Team: JumpServer team\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.3\n" + +#: acls/apps.py:7 +msgid "Acls" +msgstr "访问控制" + +#: acls/models/base.py:20 tickets/const.py:45 +#: tickets/templates/tickets/approve_check_password.html:49 +msgid "Reject" +msgstr "拒绝" + +#: acls/models/base.py:21 +msgid "Accept" +msgstr "接受" + +#: acls/models/base.py:22 +msgid "Review" +msgstr "审批" + +#: acls/models/base.py:71 acls/models/command_acl.py:21 +#: acls/serializers/base.py:34 applications/models.py:9 +#: assets/models/_user.py:33 assets/models/asset/common.py:92 +#: assets/models/asset/common.py:101 assets/models/base.py:64 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/models/platform.py:20 assets/models/platform.py:71 +#: assets/serializers/asset/common.py:87 assets/serializers/platform.py:121 +#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 +#: ops/models/adhoc.py:23 ops/models/celery.py:15 ops/models/job.py:24 +#: ops/models/playbook.py:14 orgs/models.py:67 +#: perms/models/asset_permission.py:55 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 +#: terminal/models/applet/applet.py:22 terminal/models/component/endpoint.py:12 +#: terminal/models/component/endpoint.py:86 +#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 +#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: users/models/group.py:13 users/models/user.py:675 +#: xpack/plugins/cloud/models.py:28 +msgid "Name" +msgstr "名称" + +#: acls/models/base.py:73 assets/models/_user.py:47 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:89 +msgid "Priority" +msgstr "优先级" + +#: acls/models/base.py:74 assets/models/_user.py:47 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:90 +msgid "1-100, the lower the value will be match first" +msgstr "优先级可选范围为 1-100 (数值越小越优先)" + +#: acls/models/base.py:77 acls/serializers/base.py:63 +#: assets/models/cmd_filter.py:81 audits/models.py:51 audits/serializers.py:73 +#: authentication/serializers/connect_token_secret.py:108 +#: authentication/templates/authentication/_access_key_modal.html:34 +msgid "Action" +msgstr "动作" + +#: acls/models/base.py:78 acls/serializers/base.py:59 +#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 +#: authentication/serializers/connect_token_secret.py:81 +msgid "Reviewers" +msgstr "审批人" + +#: acls/models/base.py:79 authentication/models/access_key.py:17 +#: authentication/templates/authentication/_access_key_modal.html:32 +#: perms/models/asset_permission.py:75 terminal/models/session/sharing.py:27 +#: tickets/const.py:37 +msgid "Active" +msgstr "激活中" + +#: acls/models/base.py:91 acls/models/login_acl.py:13 +#: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 +#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:30 +#: audits/models.py:49 audits/models.py:93 +#: authentication/models/connection_token.py:28 +#: authentication/models/sso_token.py:16 +#: notifications/models/notification.py:12 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:57 +#: perms/serializers/permission.py:23 rbac/builtin.py:120 +#: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:13 +#: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 +#: terminal/notifications.py:94 terminal/notifications.py:142 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:900 +#: users/models/user.py:931 users/serializers/group.py:19 +msgid "User" +msgstr "用户" + +#: acls/models/base.py:93 acls/serializers/base.py:56 +#: assets/models/account.py:50 assets/models/asset/common.py:94 +#: assets/models/asset/common.py:221 assets/models/cmd_filter.py:36 +#: assets/models/gathered_user.py:12 assets/serializers/account/account.py:76 +#: assets/serializers/automations/change_secret.py:100 +#: assets/serializers/automations/change_secret.py:122 +#: assets/serializers/domain.py:19 assets/serializers/gathered_user.py:11 +#: assets/serializers/label.py:27 audits/models.py:34 +#: authentication/models/connection_token.py:32 +#: perms/models/asset_permission.py:63 perms/serializers/permission.py:27 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 +#: terminal/models/session/session.py:31 terminal/notifications.py:93 +#: xpack/plugins/cloud/models.py:220 +msgid "Asset" +msgstr "资产" + +#: acls/models/base.py:95 acls/serializers/base.py:57 +#: assets/models/account.py:60 +#: assets/serializers/automations/change_secret.py:101 +#: assets/serializers/automations/change_secret.py:123 audits/models.py:35 +#: ops/models/base.py:18 terminal/backends/command/models.py:22 +#: terminal/models/session/session.py:33 +#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 +msgid "Account" +msgstr "账号" + +#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 +#: terminal/backends/command/serializers.py:15 +#: terminal/models/session/session.py:41 terminal/serializers/session.py:19 +#: terminal/templates/terminal/_msg_command_alert.html:12 +#: terminal/templates/terminal/_msg_command_execute_alert.html:10 +msgid "Command" +msgstr "命令" + +#: acls/models/command_acl.py:17 assets/models/cmd_filter.py:59 +msgid "Regex" +msgstr "正则表达式" + +#: acls/models/command_acl.py:24 acls/serializers/command_acl.py:19 +#: applications/models.py:14 assets/models/_user.py:46 +#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 +#: assets/models/platform.py:73 assets/serializers/asset/common.py:63 +#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:86 +#: audits/serializers.py:45 +#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:32 +#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:26 +#: terminal/models/component/storage.py:57 +#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:33 +#: terminal/serializers/session.py:25 tickets/models/comment.py:26 +#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 +#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:54 +#: tickets/serializers/ticket/ticket.py:19 +msgid "Type" +msgstr "类型" + +#: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 +#: settings/serializers/basic.py:10 xpack/plugins/license/models.py:29 +msgid "Content" +msgstr "内容" + +#: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 +msgid "One line one command" +msgstr "每行一个命令" + +#: acls/models/command_acl.py:27 assets/models/cmd_filter.py:80 +msgid "Ignore case" +msgstr "忽略大小写" + +#: acls/models/command_acl.py:33 acls/serializers/command_acl.py:29 +#: authentication/serializers/connect_token_secret.py:78 +msgid "Command group" +msgstr "命令组" + +#: acls/models/command_acl.py:86 +msgid "The generated regular expression is incorrect: {}" +msgstr "生成的正则表达式有误" + +#: acls/models/command_acl.py:96 +msgid "Commands" +msgstr "命令" + +#: acls/models/command_acl.py:100 +msgid "Command acl" +msgstr "命令过滤" + +#: acls/models/command_acl.py:109 tickets/const.py:11 +msgid "Command confirm" +msgstr "命令复核" + +#: acls/models/login_acl.py:16 +msgid "Rule" +msgstr "规则" + +#: acls/models/login_acl.py:19 +msgid "Login acl" +msgstr "登录访问控制" + +#: acls/models/login_acl.py:54 tickets/const.py:10 +msgid "Login confirm" +msgstr "登录复核" + +#: acls/models/login_asset_acl.py:10 +msgid "Login asset acl" +msgstr "登录资产访问控制" + +#: acls/models/login_asset_acl.py:20 tickets/const.py:12 +msgid "Login asset confirm" +msgstr "登录资产复核" + +#: acls/serializers/base.py:10 acls/serializers/login_acl.py:16 +msgid "Format for comma-delimited string, with * indicating a match all. " +msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " + +#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: assets/models/_user.py:34 assets/models/base.py:65 +#: assets/models/gathered_user.py:13 audits/models.py:109 +#: authentication/forms.py:25 authentication/forms.py:27 +#: authentication/models/temp_token.py:9 +#: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 +#: users/forms/profile.py:32 users/forms/profile.py:112 +#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 +msgid "Username" +msgstr "用户名" + +#: acls/serializers/base.py:25 +msgid "" +"Format for comma-delimited string, with * indicating a match all. Such as: " +"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:" +"db8:1a:1110::/64 (Domain name support)" +msgstr "" +"格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, " +"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" + +#: acls/serializers/base.py:40 assets/serializers/asset/host.py:40 +msgid "IP/Host" +msgstr "IP/主机" + +#: acls/serializers/base.py:90 tickets/serializers/ticket/ticket.py:78 +msgid "The organization `{}` does not exist" +msgstr "组织 `{}` 不存在" + +#: acls/serializers/base.py:96 +msgid "None of the reviewers belong to Organization `{}`" +msgstr "所有复核人都不属于组织 `{}`" + +#: acls/serializers/rules/rules.py:20 +#: xpack/plugins/cloud/serializers/task.py:22 +msgid "IP address invalid: `{}`" +msgstr "IP 地址无效: `{}`" + +#: acls/serializers/rules/rules.py:25 +msgid "" +"Format for comma-delimited string, with * indicating a match all. Such as: " +"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:" +"db8:1a:1110::/64 " +msgstr "" +"格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, " +"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" + +#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:102 +#: authentication/templates/authentication/_msg_oauth_bind.html:12 +#: authentication/templates/authentication/_msg_rest_password_success.html:8 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:8 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 +msgid "IP" +msgstr "IP" + +#: acls/serializers/rules/rules.py:35 +msgid "Time Period" +msgstr "时段" + +#: applications/apps.py:9 +msgid "Applications" +msgstr "应用管理" + +#: applications/models.py:11 assets/models/label.py:21 +#: assets/models/platform.py:72 assets/serializers/asset/common.py:62 +#: assets/serializers/cagegory.py:8 assets/serializers/platform.py:87 +#: assets/serializers/platform.py:122 perms/serializers/user_permission.py:23 +#: settings/models.py:35 tickets/models/ticket/apply_application.py:13 +msgid "Category" +msgstr "类别" + +#: applications/models.py:16 xpack/plugins/cloud/models.py:33 +#: xpack/plugins/cloud/serializers/account.py:62 +msgid "Attrs" +msgstr "属性" + +#: applications/models.py:19 +msgid "Application" +msgstr "应用程序" + +#: applications/models.py:23 +msgid "Can match application" +msgstr "匹配应用" + +#: applications/serializers/attrs/application_type/clickhouse.py:11 +#: assets/models/asset/common.py:93 assets/models/platform.py:21 +#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 +#: xpack/plugins/cloud/serializers/account_attrs.py:73 +msgid "Port" +msgstr "端口" + +#: applications/serializers/attrs/application_type/clickhouse.py:13 +msgid "" +"Typically, the port is 9000,the HTTP interface and the native interface use " +"different ports" +msgstr "默认端口为9000, HTTP接口和本机接口使用不同的端口" + +#: assets/api/automations/base.py:77 +msgid "The parameter 'action' must be [{}]" +msgstr "参数 'action' 必须是 [{}]" + +#: assets/api/domain.py:56 +msgid "Number required" +msgstr "需要为数字" + +#: assets/api/node.py:56 +msgid "You can't update the root node name" +msgstr "不能修改根节点名称" + +#: assets/api/node.py:63 +msgid "You can't delete the root node ({})" +msgstr "不能删除根节点 ({})" + +#: assets/api/node.py:66 +msgid "Deletion failed and the node contains assets" +msgstr "删除失败,节点包含资产" + +#: assets/apps.py:9 +msgid "App assets" +msgstr "资产管理" + +#: assets/automations/base/manager.py:123 +msgid "{} disabled" +msgstr "{} 已禁用" + +#: assets/const/account.py:6 audits/const.py:6 audits/const.py:64 +#: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 +#: common/utils/ip/utils.py:84 +msgid "Unknown" +msgstr "未知" + +#: assets/const/account.py:7 +msgid "Ok" +msgstr "成功" + +#: assets/const/account.py:8 +#: assets/serializers/automations/change_secret.py:118 +#: assets/serializers/automations/change_secret.py:146 audits/const.py:75 +#: common/const/choices.py:19 ops/const.py:51 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失败" + +#: assets/const/account.py:12 assets/models/_user.py:35 +#: audits/signal_handlers.py:49 authentication/confirm/password.py:9 +#: authentication/forms.py:32 +#: authentication/templates/authentication/login.html:288 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 +#: users/forms/profile.py:22 users/serializers/user.py:97 +#: users/templates/users/_msg_user_created.html:13 +#: users/templates/users/user_password_verify.html:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 +msgid "Password" +msgstr "密码" + +#: assets/const/account.py:13 +msgid "SSH key" +msgstr "SSH 密钥" + +#: assets/const/account.py:14 authentication/models/access_key.py:33 +msgid "Access key" +msgstr "访问密钥" + +#: assets/const/account.py:15 assets/models/_user.py:38 +#: authentication/models/sso_token.py:14 +msgid "Token" +msgstr "令牌" + +#: assets/const/automation.py:13 +msgid "Ping" +msgstr "" + +#: assets/const/automation.py:14 +msgid "Gather facts" +msgstr "收集资产信息" + +#: assets/const/automation.py:15 +msgid "Create account" +msgstr "创建账号" + +#: assets/const/automation.py:16 +msgid "Change secret" +msgstr "更改密码" + +#: assets/const/automation.py:17 +msgid "Verify account" +msgstr "验证账号" + +#: assets/const/automation.py:18 +msgid "Gather accounts" +msgstr "收集账号" + +#: assets/const/automation.py:38 assets/serializers/account/base.py:29 +msgid "Specific" +msgstr "特有的" + +#: assets/const/automation.py:39 ops/const.py:20 +msgid "All assets use the same random password" +msgstr "使用相同的随机密码" + +#: assets/const/automation.py:40 ops/const.py:21 +msgid "All assets use different random password" +msgstr "使用不同的随机密码" + +#: assets/const/automation.py:44 ops/const.py:13 +msgid "Append SSH KEY" +msgstr "追加" + +#: assets/const/automation.py:45 ops/const.py:14 +msgid "Empty and append SSH KEY" +msgstr "清空所有并添加" + +#: assets/const/automation.py:46 ops/const.py:15 +msgid "Replace (The key generated by JumpServer) " +msgstr "替换 (由 JumpServer 生成的密钥)" + +#: assets/const/category.py:11 settings/serializers/auth/radius.py:16 +#: settings/serializers/auth/sms.py:67 terminal/models/applet/applet.py:129 +#: terminal/models/component/endpoint.py:13 +#: xpack/plugins/cloud/serializers/account_attrs.py:72 +msgid "Host" +msgstr "主机" + +#: assets/const/category.py:12 +msgid "Device" +msgstr "网络设备" + +#: assets/const/category.py:13 assets/models/asset/database.py:8 +#: assets/models/asset/database.py:34 +msgid "Database" +msgstr "数据库" + +#: assets/const/category.py:14 +msgid "Cloud service" +msgstr "云服务" + +#: assets/const/category.py:15 audits/const.py:62 +#: terminal/models/applet/applet.py:20 +msgid "Web" +msgstr "Web" + +#: assets/const/device.py:7 terminal/models/applet/applet.py:19 +#: tickets/const.py:8 +msgid "General" +msgstr "一般" + +#: assets/const/device.py:8 +msgid "Switch" +msgstr "交换机" + +#: assets/const/device.py:9 +msgid "Router" +msgstr "路由器" + +#: assets/const/device.py:10 +msgid "Firewall" +msgstr "防火墙" + +#: assets/const/types.py:181 +msgid "All types" +msgstr "所有类型" + +#: assets/const/web.py:7 +msgid "Website" +msgstr "网站" + +#: assets/models/_user.py:24 +msgid "Automatic managed" +msgstr "托管密码" + +#: assets/models/_user.py:25 +msgid "Manually input" +msgstr "手动输入" + +#: assets/models/_user.py:29 +msgid "Common user" +msgstr "普通用户" + +#: assets/models/_user.py:30 +msgid "Admin user" +msgstr "特权用户" + +#: assets/models/_user.py:36 +msgid "SSH private key" +msgstr "SSH密钥" + +#: assets/models/_user.py:37 +msgid "SSH public key" +msgstr "SSH公钥" + +#: assets/models/_user.py:40 assets/models/cmd_filter.py:40 +#: assets/models/cmd_filter.py:88 assets/models/group.py:23 +#: assets/models/platform.py:76 common/db/models.py:78 ops/models/adhoc.py:29 +#: ops/models/job.py:40 ops/models/playbook.py:17 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:31 +#: terminal/models/applet/applet.py:131 terminal/models/applet/host.py:110 +#: terminal/models/component/endpoint.py:20 +#: terminal/models/component/endpoint.py:96 +#: terminal/models/session/session.py:45 tickets/models/comment.py:32 +#: tickets/models/ticket/general.py:297 users/models/user.py:714 +#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:119 +msgid "Comment" +msgstr "备注" + +#: assets/models/_user.py:41 assets/models/automations/base.py:91 +#: assets/models/cmd_filter.py:41 assets/models/group.py:22 +#: common/db/models.py:76 ops/models/base.py:54 ops/models/job.py:105 +#: users/models/user.py:932 +msgid "Date created" +msgstr "创建日期" + +#: assets/models/_user.py:42 assets/models/cmd_filter.py:42 +#: common/db/models.py:77 +msgid "Date updated" +msgstr "更新日期" + +#: assets/models/_user.py:43 assets/models/cmd_filter.py:44 +#: assets/models/cmd_filter.py:91 assets/models/group.py:21 +#: common/db/models.py:74 users/models/user.py:722 +#: users/serializers/group.py:33 +msgid "Created by" +msgstr "创建者" + +#: assets/models/_user.py:45 +msgid "Username same with user" +msgstr "用户名与用户相同" + +#: assets/models/_user.py:48 authentication/models/connection_token.py:37 +#: authentication/serializers/connect_token_secret.py:103 +#: terminal/models/applet/applet.py:29 terminal/serializers/session.py:24 +#: terminal/serializers/session.py:40 terminal/serializers/storage.py:68 +msgid "Protocol" +msgstr "协议" + +#: assets/models/_user.py:49 +msgid "Auto push" +msgstr "自动推送" + +#: assets/models/_user.py:50 +msgid "Sudo" +msgstr "Sudo" + +#: assets/models/_user.py:51 ops/const.py:44 ops/models/adhoc.py:19 +msgid "Shell" +msgstr "Shell" + +#: assets/models/_user.py:52 +msgid "Login mode" +msgstr "认证方式" + +#: assets/models/_user.py:53 +msgid "SFTP Root" +msgstr "SFTP根路径" + +#: assets/models/_user.py:54 +msgid "Home" +msgstr "家目录" + +#: assets/models/_user.py:55 +msgid "System groups" +msgstr "用户组" + +#: assets/models/_user.py:58 +msgid "User switch" +msgstr "用户切换" + +#: assets/models/_user.py:59 +msgid "Switch from" +msgstr "切换自" + +#: assets/models/_user.py:65 +msgid "System user" +msgstr "系统用户" + +#: assets/models/_user.py:67 +msgid "Can match system user" +msgstr "可以匹配系统用户" + +#: assets/models/account.py:44 common/db/fields.py:232 +#: settings/serializers/terminal.py:14 +msgid "All" +msgstr "全部" + +#: assets/models/account.py:45 +msgid "Manual input" +msgstr "手动输入" + +#: assets/models/account.py:46 +msgid "Dynamic user" +msgstr "同名账号" + +#: assets/models/account.py:54 assets/serializers/account/account.py:79 +#: authentication/serializers/connect_token_secret.py:48 +msgid "Su from" +msgstr "切换自" + +#: assets/models/account.py:56 settings/serializers/auth/cas.py:20 +#: terminal/models/applet/applet.py:24 +msgid "Version" +msgstr "版本" + +#: assets/models/account.py:66 +msgid "Can view asset account secret" +msgstr "可以查看资产账号密码" + +#: assets/models/account.py:67 +msgid "Can change asset account secret" +msgstr "可以更改资产账号密码" + +#: assets/models/account.py:68 +msgid "Can view asset history account" +msgstr "可以查看资产历史账号" + +#: assets/models/account.py:69 +msgid "Can view asset history account secret" +msgstr "可以查看资产历史账号密码" + +#: assets/models/account.py:106 assets/serializers/account/account.py:15 +msgid "Account template" +msgstr "账号模版" + +#: assets/models/account.py:111 +msgid "Can view asset account template secret" +msgstr "可以查看资产账号密码" + +#: assets/models/account.py:112 +msgid "Can change asset account template secret" +msgstr "可以更改账号模版密码" + +#: assets/models/asset/common.py:103 assets/models/platform.py:109 +#: assets/serializers/asset/common.py:65 +#: authentication/serializers/connect_token_secret.py:107 +#: perms/serializers/user_permission.py:21 +#: xpack/plugins/cloud/serializers/account_attrs.py:179 +msgid "Platform" +msgstr "系统平台" + +#: assets/models/asset/common.py:105 assets/models/domain.py:21 +#: assets/serializers/asset/common.py:64 +#: authentication/serializers/connect_token_secret.py:125 +msgid "Domain" +msgstr "网域" + +#: assets/models/asset/common.py:107 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/serializers/asset/common.py:66 +#: assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:66 +msgid "Nodes" +msgstr "节点" + +#: assets/models/asset/common.py:108 assets/models/automations/base.py:21 +#: assets/models/base.py:71 assets/models/cmd_filter.py:39 +#: assets/models/label.py:22 +#: authentication/serializers/connect_token_secret.py:106 +#: terminal/models/applet/applet.py:27 users/serializers/user.py:158 +msgid "Is active" +msgstr "激活" + +#: assets/models/asset/common.py:109 assets/serializers/asset/common.py:67 +msgid "Labels" +msgstr "标签管理" + +#: assets/models/asset/common.py:224 +msgid "Can refresh asset hardware info" +msgstr "可以更新资产硬件信息" + +#: assets/models/asset/common.py:225 +msgid "Can test asset connectivity" +msgstr "可以测试资产连接性" + +#: assets/models/asset/common.py:226 +msgid "Can push account to asset" +msgstr "可以推送账号到资产" + +#: assets/models/asset/common.py:227 +msgid "Can match asset" +msgstr "可以匹配资产" + +#: assets/models/asset/common.py:228 +msgid "Add asset to node" +msgstr "添加资产到节点" + +#: assets/models/asset/common.py:229 +msgid "Move asset to node" +msgstr "移动资产到节点" + +#: assets/models/asset/database.py:9 settings/serializers/email.py:37 +msgid "Use SSL" +msgstr "使用 SSL" + +#: assets/models/asset/database.py:10 +msgid "CA cert" +msgstr "CA 证书" + +#: assets/models/asset/database.py:11 +msgid "Client cert" +msgstr "客户端证书" + +#: assets/models/asset/database.py:12 +msgid "Client key" +msgstr "客户端密钥" + +#: assets/models/asset/database.py:13 +msgid "Allow invalid cert" +msgstr "忽略证书校验" + +#: assets/models/asset/web.py:9 audits/const.py:68 +#: terminal/serializers/applet_host.py:25 +msgid "Disabled" +msgstr "禁用" + +#: assets/models/asset/web.py:10 settings/serializers/auth/base.py:10 +#: settings/serializers/basic.py:27 +msgid "Basic" +msgstr "基本" + +#: assets/models/asset/web.py:11 assets/models/asset/web.py:17 +msgid "Script" +msgstr "" + +#: assets/models/asset/web.py:13 assets/serializers/platform.py:28 +msgid "Autofill" +msgstr "自动代填" + +#: assets/models/asset/web.py:14 assets/serializers/platform.py:30 +msgid "Username selector" +msgstr "用户名选择器" + +#: assets/models/asset/web.py:15 assets/serializers/platform.py:33 +msgid "Password selector" +msgstr "密码选择器" + +#: assets/models/asset/web.py:16 assets/serializers/platform.py:36 +msgid "Submit selector" +msgstr "确认按钮选择器" + +#: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 +#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:69 +#: perms/serializers/permission.py:32 rbac/tree.py:36 +msgid "Accounts" +msgstr "账号管理" + +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:34 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "资产" + +#: assets/models/automations/base.py:81 assets/models/automations/base.py:88 +msgid "Automation task" +msgstr "自动化任务" + +#: assets/models/automations/base.py:90 audits/models.py:129 +#: audits/serializers.py:46 ops/models/base.py:49 ops/models/job.py:98 +#: terminal/models/applet/applet.py:130 terminal/models/applet/host.py:107 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 +#: tickets/models/ticket/general.py:283 tickets/serializers/ticket/ticket.py:20 +#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:224 +msgid "Status" +msgstr "状态" + +#: assets/models/automations/base.py:92 assets/models/backup.py:73 +#: audits/models.py:41 ops/models/base.py:55 ops/models/celery.py:60 +#: ops/models/job.py:106 perms/models/asset_permission.py:71 +#: terminal/models/applet/host.py:108 terminal/models/session/session.py:43 +#: tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:19 +msgid "Date start" +msgstr "开始日期" + +#: assets/models/automations/base.py:93 +#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 +#: ops/models/celery.py:61 ops/models/job.py:107 +#: terminal/models/applet/host.py:109 +msgid "Date finished" +msgstr "结束日期" + +#: assets/models/automations/base.py:95 +#: assets/serializers/automations/base.py:39 +msgid "Automation snapshot" +msgstr "工单快照" + +#: assets/models/automations/base.py:99 assets/models/backup.py:84 +#: assets/serializers/automations/base.py:41 +msgid "Trigger mode" +msgstr "触发模式" + +#: assets/models/automations/base.py:103 +#: assets/serializers/automations/change_secret.py:103 +msgid "Automation task execution" +msgstr "自动化任务执行历史" + +#: assets/models/automations/base.py:105 +msgid "Can view change secret execution" +msgstr "查看改密执行" + +#: assets/models/automations/base.py:106 +msgid "Can add change secret execution" +msgstr "创建改密执行" + +#: assets/models/automations/base.py:107 +msgid "Can view gather accounts execution" +msgstr "查看收集账号执行" + +#: assets/models/automations/base.py:108 +msgid "Can add gather accounts execution" +msgstr "创建收集账号执行" + +#: assets/models/automations/change_secret.py:15 assets/models/base.py:67 +#: assets/serializers/account/account.py:112 assets/serializers/base.py:13 +#: authentication/serializers/connect_token_secret.py:39 +#: authentication/serializers/connect_token_secret.py:49 +msgid "Secret type" +msgstr "密文类型" + +#: assets/models/automations/change_secret.py:19 +#: assets/serializers/automations/change_secret.py:25 +msgid "Secret strategy" +msgstr "密文策略" + +#: assets/models/automations/change_secret.py:21 +#: assets/models/automations/change_secret.py:57 assets/models/base.py:69 +#: assets/serializers/base.py:16 authentication/models/temp_token.py:10 +#: authentication/templates/authentication/_access_key_modal.html:31 +#: settings/serializers/auth/radius.py:19 +msgid "Secret" +msgstr "密钥" + +#: assets/models/automations/change_secret.py:22 +msgid "Password rules" +msgstr "密码规则" + +#: assets/models/automations/change_secret.py:25 +msgid "SSH key change strategy" +msgstr "SSH 密钥策略" + +#: assets/models/automations/change_secret.py:27 assets/models/backup.py:25 +#: assets/serializers/account/backup.py:30 +#: assets/serializers/automations/change_secret.py:40 +msgid "Recipient" +msgstr "收件人" + +#: assets/models/automations/change_secret.py:34 +msgid "Change secret automation" +msgstr "自动化改密" + +#: assets/models/automations/change_secret.py:56 +msgid "Old secret" +msgstr "原密码" + +#: assets/models/automations/change_secret.py:58 +msgid "Date started" +msgstr "开始日期" + +#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 +msgid "Error" +msgstr "错误" + +#: assets/models/automations/change_secret.py:64 +msgid "Change secret record" +msgstr "改密记录" + +#: assets/models/automations/gather_accounts.py:15 +#: assets/tasks/gather_accounts.py:28 +msgid "Gather asset accounts" +msgstr "收集账号" + +#: assets/models/automations/gather_facts.py:15 +msgid "Gather asset facts" +msgstr "收集资产信息" + +#: assets/models/automations/ping.py:15 +msgid "Ping asset" +msgstr "测试资产" + +#: assets/models/automations/push_account.py:16 +msgid "Push asset account" +msgstr "账号推送" + +#: assets/models/automations/verify_account.py:15 +msgid "Verify asset account" +msgstr "账号验证" + +#: assets/models/backup.py:34 assets/models/backup.py:92 +msgid "Account backup plan" +msgstr "账号备份计划" + +#: assets/models/backup.py:76 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:186 +msgid "Time" +msgstr "时间" + +#: assets/models/backup.py:80 +msgid "Account backup snapshot" +msgstr "账号备份快照" + +#: assets/models/backup.py:87 audits/models.py:124 +#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "原因" + +#: assets/models/backup.py:89 +#: assets/serializers/automations/change_secret.py:99 +#: assets/serializers/automations/change_secret.py:124 +#: terminal/serializers/session.py:44 +msgid "Is success" +msgstr "是否成功" + +#: assets/models/backup.py:96 +msgid "Account backup execution" +msgstr "账号备份执行" + +#: assets/models/base.py:26 +msgid "Connectivity" +msgstr "可连接性" + +#: assets/models/base.py:28 authentication/models/temp_token.py:12 +msgid "Date verified" +msgstr "校验日期" + +#: assets/models/base.py:70 +msgid "Privileged" +msgstr "特权账号" + +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:60 +#: perms/serializers/permission.py:25 users/models/group.py:25 +#: users/models/user.py:681 +msgid "User group" +msgstr "用户组" + +#: assets/models/cmd_filter.py:52 +msgid "Command filter" +msgstr "命令过滤器" + +#: assets/models/cmd_filter.py:66 +msgid "Deny" +msgstr "拒绝" + +#: assets/models/cmd_filter.py:67 +msgid "Allow" +msgstr "允许" + +#: assets/models/cmd_filter.py:68 +msgid "Reconfirm" +msgstr "复核" + +#: assets/models/cmd_filter.py:72 +msgid "Filter" +msgstr "过滤器" + +#: assets/models/cmd_filter.py:95 +msgid "Command filter rule" +msgstr "命令过滤规则" + +#: assets/models/gateway.py:40 assets/serializers/domain.py:16 +msgid "Gateway" +msgstr "网关" + +#: assets/models/gateway.py:62 authentication/models/connection_token.py:104 +msgid "No account" +msgstr "没有账号" + +#: assets/models/gateway.py:84 +#, python-brace-format +msgid "Unable to connect to port {port} on {address}" +msgstr "无法连接到 {port} 上的端口 {address}" + +#: assets/models/gateway.py:87 authentication/middleware.py:76 +#: xpack/plugins/cloud/providers/fc.py:48 +msgid "Authentication failed" +msgstr "认证失败" + +#: assets/models/gateway.py:89 assets/models/gateway.py:116 +msgid "Connect failed" +msgstr "连接失败" + +#: assets/models/gathered_user.py:14 +msgid "Present" +msgstr "存在" + +#: assets/models/gathered_user.py:15 +msgid "Date last login" +msgstr "最后登录日期" + +#: assets/models/gathered_user.py:16 +msgid "IP last login" +msgstr "最后登录IP" + +#: assets/models/gathered_user.py:27 +msgid "GatherUser" +msgstr "收集用户" + +#: assets/models/group.py:30 +msgid "Asset group" +msgstr "资产组" + +#: assets/models/group.py:34 assets/models/platform.py:18 +#: xpack/plugins/cloud/providers/nutanix.py:30 +msgid "Default" +msgstr "默认" + +#: assets/models/group.py:34 +msgid "Default asset group" +msgstr "默认资产组" + +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:917 +msgid "System" +msgstr "系统" + +#: assets/models/label.py:19 assets/models/node.py:552 +#: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 +#: authentication/models/connection_token.py:25 +#: authentication/serializers/connect_token_secret.py:114 +#: common/drf/serializers/common.py:82 settings/models.py:34 +msgid "Value" +msgstr "值" + +#: assets/models/label.py:40 assets/serializers/cagegory.py:6 +#: assets/serializers/cagegory.py:13 +#: authentication/serializers/connect_token_secret.py:113 +#: common/drf/serializers/common.py:81 settings/serializers/sms.py:7 +msgid "Label" +msgstr "标签" + +#: assets/models/node.py:153 +msgid "New node" +msgstr "新节点" + +#: assets/models/node.py:480 +msgid "empty" +msgstr "空" + +#: assets/models/node.py:551 perms/models/perm_node.py:27 +msgid "Key" +msgstr "键" + +#: assets/models/node.py:553 assets/serializers/node.py:20 +msgid "Full value" +msgstr "全称" + +#: assets/models/node.py:557 perms/models/perm_node.py:29 +msgid "Parent key" +msgstr "ssh私钥" + +#: assets/models/node.py:566 perms/serializers/permission.py:28 +#: xpack/plugins/cloud/models.py:96 +msgid "Node" +msgstr "节点" + +#: assets/models/node.py:569 +msgid "Can match node" +msgstr "可以匹配节点" + +#: assets/models/platform.py:19 +msgid "Required" +msgstr "必须的" + +#: assets/models/platform.py:22 settings/serializers/settings.py:61 +#: users/templates/users/reset_password.html:29 +msgid "Setting" +msgstr "设置" + +#: assets/models/platform.py:41 audits/const.py:69 settings/models.py:37 +#: terminal/serializers/applet_host.py:26 +msgid "Enabled" +msgstr "启用" + +#: assets/models/platform.py:42 +msgid "Ansible config" +msgstr "Ansible 配置" + +#: assets/models/platform.py:43 +msgid "Ping enabled" +msgstr "启用资产探活" + +#: assets/models/platform.py:44 +msgid "Ping method" +msgstr "资产探活方式" + +#: assets/models/platform.py:45 assets/models/platform.py:55 +msgid "Gather facts enabled" +msgstr "收集资产信息" + +#: assets/models/platform.py:46 assets/models/platform.py:57 +msgid "Gather facts method" +msgstr "收集信息方式" + +#: assets/models/platform.py:47 +msgid "Push account enabled" +msgstr "启用账号推送" + +#: assets/models/platform.py:48 +msgid "Push account method" +msgstr "账号推送方式" + +#: assets/models/platform.py:49 +msgid "Change password enabled" +msgstr "开启账号改密" + +#: assets/models/platform.py:51 +msgid "Change password method" +msgstr "更改密码方式" + +#: assets/models/platform.py:52 +msgid "Verify account enabled" +msgstr "开启账号验证" + +#: assets/models/platform.py:54 +msgid "Verify account method" +msgstr "账号验证方式" + +#: assets/models/platform.py:74 tickets/models/ticket/general.py:300 +msgid "Meta" +msgstr "元数据" + +#: assets/models/platform.py:75 +msgid "Internal" +msgstr "内置" + +#: assets/models/platform.py:79 assets/serializers/platform.py:84 +msgid "Charset" +msgstr "编码" + +#: assets/models/platform.py:81 +msgid "Domain enabled" +msgstr "启用网域" + +#: assets/models/platform.py:82 +msgid "Protocols enabled" +msgstr "启用协议" + +#: assets/models/platform.py:84 +msgid "Su enabled" +msgstr "启用账号切换" + +#: assets/models/platform.py:85 +msgid "Su method" +msgstr "账号切换方式" + +#: assets/models/platform.py:87 assets/serializers/platform.py:91 +msgid "Automation" +msgstr "自动化" + +#: assets/models/utils.py:19 +#, python-format +msgid "%(value)s is not an even number" +msgstr "%(value)s is not an even number" + +#: assets/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "账号备份任务结果通知" + +#: assets/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "{} - 账号备份任务已完成, 详情见附件" + +#: assets/notifications.py:20 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" +"置加密密码" + +#: assets/notifications.py:31 +msgid "Notification of implementation result of encryption change plan" +msgstr "改密计划任务结果通知" + +#: assets/notifications.py:41 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} - 改密任务已完成, 详情见附件" + +#: assets/notifications.py:42 +msgid "" +"{} - The encryption change task has been completed: the encryption password " +"has not been set - please go to personal information -> file encryption " +"password to set the encryption password" +msgstr "" +"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" +"密密码" + +#: assets/serializers/account/account.py:18 +msgid "Push now" +msgstr "立即推送" + +#: assets/serializers/account/account.py:20 +#: assets/serializers/account/base.py:13 +msgid "Has secret" +msgstr "已托管密码" + +#: assets/serializers/account/account.py:27 +msgid "Account template not found" +msgstr "账号模版未找到" + +#: assets/serializers/account/account.py:72 +msgid "Asset not found" +msgstr "资产不存在" + +#: assets/serializers/account/backup.py:29 +#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 +#: settings/serializers/auth/ldap.py:66 +msgid "Periodic perform" +msgstr "定时执行" + +#: assets/serializers/account/backup.py:31 +#: assets/serializers/automations/change_secret.py:41 +msgid "Currently only mail sending is supported" +msgstr "当前只支持邮件发送" + +#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:89 +#: authentication/serializers/connect_token_secret.py:27 +#: authentication/serializers/connect_token_secret.py:65 +#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:107 +#: xpack/plugins/cloud/serializers/task.py:38 +msgid "Protocols" +msgstr "协议组" + +#: assets/serializers/asset/common.py:88 +msgid "Address" +msgstr "地址" + +#: assets/serializers/asset/common.py:89 +msgid "Node path" +msgstr "节点路径" + +#: assets/serializers/asset/common.py:157 +msgid "Platform not exist" +msgstr "平台不存在" + +#: assets/serializers/asset/common.py:173 +msgid "Protocol is required: {}" +msgstr "协议是必填的: {}" + +#: assets/serializers/asset/host.py:12 +msgid "Vendor" +msgstr "制造商" + +#: assets/serializers/asset/host.py:13 +msgid "Model" +msgstr "型号" + +#: assets/serializers/asset/host.py:14 tickets/models/ticket/general.py:299 +msgid "Serial number" +msgstr "序列号" + +#: assets/serializers/asset/host.py:16 +msgid "CPU model" +msgstr "CPU型号" + +#: assets/serializers/asset/host.py:17 +msgid "CPU count" +msgstr "CPU数量" + +#: assets/serializers/asset/host.py:18 +msgid "CPU cores" +msgstr "CPU核数" + +#: assets/serializers/asset/host.py:19 +msgid "CPU vcpus" +msgstr "CPU总数" + +#: assets/serializers/asset/host.py:20 +msgid "Memory" +msgstr "内存" + +#: assets/serializers/asset/host.py:21 +msgid "Disk total" +msgstr "硬盘大小" + +#: assets/serializers/asset/host.py:22 +msgid "Disk info" +msgstr "硬盘信息" + +#: assets/serializers/asset/host.py:24 +#: authentication/serializers/connect_token_secret.py:104 +msgid "OS" +msgstr "操作系统" + +#: assets/serializers/asset/host.py:25 +msgid "OS version" +msgstr "系统版本" + +#: assets/serializers/asset/host.py:26 +msgid "OS arch" +msgstr "系统架构" + +#: assets/serializers/asset/host.py:27 +msgid "Hostname raw" +msgstr "主机名原始" + +#: assets/serializers/asset/host.py:28 +msgid "Asset number" +msgstr "资产编号" + +#: assets/serializers/automations/change_secret.py:28 +msgid "SSH Key strategy" +msgstr "SSH 密钥策略" + +#: assets/serializers/automations/change_secret.py:70 +msgid "* Please enter the correct password length" +msgstr "* 请输入正确的密码长度" + +#: assets/serializers/automations/change_secret.py:73 +msgid "* Password length range 6-30 bits" +msgstr "* 密码长度范围 6-30 位" + +#: assets/serializers/automations/change_secret.py:117 +#: assets/serializers/automations/change_secret.py:145 audits/const.py:74 +#: audits/models.py:40 common/const/choices.py:18 ops/const.py:50 +#: ops/serializers/celery.py:39 terminal/models/session/sharing.py:103 +#: tickets/views/approve.py:114 +msgid "Success" +msgstr "成功" + +#: assets/serializers/automations/gather_accounts.py:23 +msgid "Executed amount" +msgstr "执行次数" + +#: assets/serializers/base.py:21 +msgid "Key password" +msgstr "密钥密码" + +#: assets/serializers/cagegory.py:9 +msgid "Constraints" +msgstr "" + +#: assets/serializers/cagegory.py:15 +msgid "Types" +msgstr "类型" + +#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:9 +msgid "Hostname" +msgstr "主机名" + +#: assets/serializers/label.py:12 +msgid "Assets amount" +msgstr "资产数量" + +#: assets/serializers/node.py:17 +msgid "value" +msgstr "值" + +#: assets/serializers/node.py:31 +msgid "Can't contains: /" +msgstr "不能包含: /" + +#: assets/serializers/node.py:41 +msgid "The same level node name cannot be the same" +msgstr "同级别节点名字不能重复" + +#: assets/serializers/platform.py:24 +msgid "SFTP enabled" +msgstr "SFTP 已启用" + +#: assets/serializers/platform.py:25 +msgid "SFTP home" +msgstr "SFTP根路径" + +#: assets/serializers/platform.py:71 +msgid "Primary" +msgstr "主要的" + +#: assets/serializers/utils.py:13 +msgid "Password can not contains `{{` " +msgstr "密码不能包含 `{{` 字符" + +#: assets/serializers/utils.py:16 +msgid "Password can not contains `'` " +msgstr "密码不能包含 `'` 字符" + +#: assets/serializers/utils.py:18 +msgid "Password can not contains `\"` " +msgstr "密码不能包含 `\"` 字符" + +#: assets/serializers/utils.py:24 +msgid "private key invalid or passphrase error" +msgstr "密钥不合法或密钥密码错误" + +#: assets/tasks/automation.py:11 +msgid "Execute automation" +msgstr "执行自动化任务" + +#: assets/tasks/backup.py:13 +msgid "Execute account backup plan" +msgstr "执行账号备份计划" + +#: assets/tasks/gather_accounts.py:31 +msgid "Gather assets accounts" +msgstr "收集资产上的账号" + +#: assets/tasks/gather_facts.py:26 +msgid "Update some assets hardware info. " +msgstr "更新资产硬件信息. " + +#: assets/tasks/gather_facts.py:44 +msgid "Manually update the hardware information of assets" +msgstr "手动更新资产信息" + +#: assets/tasks/gather_facts.py:49 +msgid "Update assets hardware info: " +msgstr "更新资产硬件信息" + +#: assets/tasks/gather_facts.py:53 +msgid "Manually update the hardware information of assets under a node" +msgstr "手动更新节点下资产信息" + +#: assets/tasks/gather_facts.py:59 +msgid "Update node asset hardware information: " +msgstr "更新节点资产硬件信息: " + +#: assets/tasks/nodes_amount.py:16 +msgid "Check the amount of assets under the node" +msgstr "检查节点下资产数量" + +#: assets/tasks/nodes_amount.py:28 +msgid "" +"The task of self-checking is already running and cannot be started repeatedly" +msgstr "自检程序已经在运行,不能重复启动" + +#: assets/tasks/nodes_amount.py:34 +msgid "Periodic check the amount of assets under the node" +msgstr "周期性检查节点下资产数量" + +#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 +msgid "Test assets connectivity " +msgstr "测试资产可连接性" + +#: assets/tasks/ping.py:33 +msgid "Manually test the connectivity of a asset" +msgstr "手动测试资产连接性" + +#: assets/tasks/ping.py:43 +msgid "Manually test the connectivity of assets under a node" +msgstr "手动测试节点下资产连接性" + +#: assets/tasks/ping.py:49 +msgid "Test if the assets under the node are connectable " +msgstr "测试节点下资产是否可连接" + +#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:34 +msgid "Push accounts to assets" +msgstr "推送账号到资产" + +#: assets/tasks/utils.py:17 +msgid "Asset has been disabled, skipped: {}" +msgstr "资产已经被禁用, 跳过: {}" + +#: assets/tasks/utils.py:21 +msgid "Asset may not be support ansible, skipped: {}" +msgstr "资产或许不支持ansible, 跳过: {}" + +#: assets/tasks/utils.py:39 +msgid "For security, do not push user {}" +msgstr "为了安全,禁止推送用户 {}" + +#: assets/tasks/utils.py:55 +msgid "No assets matched, stop task" +msgstr "没有匹配到资产,结束任务" + +#: assets/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "" + +#: assets/tasks/verify_account.py:37 +msgid "Verify accounts connectivity" +msgstr "测试账号可连接性" + +#: audits/apps.py:9 +msgid "Audits" +msgstr "日志审计" + +#: audits/backends/db.py:12 +msgid "The text content is too long. Use Elasticsearch to store operation logs" +msgstr "文字内容太长。请使用 Elasticsearch 存储操作日志" + +#: audits/backends/db.py:24 audits/backends/db.py:26 +msgid "Tips" +msgstr "提示" + +#: audits/const.py:45 +msgid "Mkdir" +msgstr "创建目录" + +#: audits/const.py:46 +msgid "Rmdir" +msgstr "删除目录" + +#: audits/const.py:47 audits/const.py:57 +#: authentication/templates/authentication/_access_key_modal.html:65 +#: rbac/tree.py:232 +msgid "Delete" +msgstr "删除" + +#: audits/const.py:48 perms/const.py:13 +msgid "Upload" +msgstr "上传文件" + +#: audits/const.py:49 +msgid "Rename" +msgstr "重命名" + +#: audits/const.py:50 +msgid "Symlink" +msgstr "建立软链接" + +#: audits/const.py:51 perms/const.py:14 +msgid "Download" +msgstr "下载文件" + +#: audits/const.py:55 rbac/tree.py:230 +msgid "View" +msgstr "查看" + +#: audits/const.py:56 rbac/tree.py:231 templates/_csv_import_export.html:18 +#: templates/_csv_update_modal.html:6 +msgid "Update" +msgstr "更新" + +#: audits/const.py:58 +#: authentication/templates/authentication/_access_key_modal.html:22 +#: rbac/tree.py:229 +msgid "Create" +msgstr "创建" + +#: audits/const.py:63 settings/serializers/terminal.py:6 +#: terminal/models/applet/host.py:24 terminal/models/component/terminal.py:156 +msgid "Terminal" +msgstr "终端" + +#: audits/const.py:70 +msgid "-" +msgstr "-" + +#: audits/handler.py:134 +msgid "Yes" +msgstr "是" + +#: audits/handler.py:134 +msgid "No" +msgstr "否" + +#: audits/models.py:32 audits/models.py:55 audits/models.py:96 +#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:95 +msgid "Remote addr" +msgstr "远端地址" + +#: audits/models.py:37 audits/serializers.py:30 +msgid "Operate" +msgstr "操作" + +#: audits/models.py:39 +msgid "Filename" +msgstr "文件名" + +#: audits/models.py:44 +msgid "File transfer log" +msgstr "文件管理" + +#: audits/models.py:53 audits/serializers.py:84 +msgid "Resource Type" +msgstr "资源类型" + +#: audits/models.py:54 +msgid "Resource" +msgstr "资源" + +#: audits/models.py:56 audits/models.py:98 +#: terminal/backends/command/serializers.py:40 +msgid "Datetime" +msgstr "日期" + +#: audits/models.py:88 +msgid "Operate log" +msgstr "操作日志" + +#: audits/models.py:94 +msgid "Change by" +msgstr "修改者" + +#: audits/models.py:104 +msgid "Password change log" +msgstr "改密日志" + +#: audits/models.py:111 +msgid "Login type" +msgstr "登录方式" + +#: audits/models.py:113 tickets/models/ticket/login_confirm.py:10 +msgid "Login ip" +msgstr "登录IP" + +#: audits/models.py:115 +#: authentication/templates/authentication/_msg_different_city.html:11 +#: tickets/models/ticket/login_confirm.py:11 +msgid "Login city" +msgstr "登录城市" + +#: audits/models.py:118 audits/serializers.py:60 +msgid "User agent" +msgstr "用户代理" + +#: audits/models.py:121 audits/serializers.py:44 +#: authentication/templates/authentication/_mfa_confirm_modal.html:14 +#: users/forms/profile.py:65 users/models/user.py:698 +#: users/serializers/profile.py:126 +msgid "MFA" +msgstr "MFA" + +#: audits/models.py:131 +msgid "Date login" +msgstr "登录日期" + +#: audits/models.py:133 audits/serializers.py:62 +msgid "Authentication backend" +msgstr "认证方式" + +#: audits/models.py:174 +msgid "User login log" +msgstr "用户登录日志" + +#: audits/serializers.py:61 +msgid "Reason display" +msgstr "原因描述" + +#: audits/signal_handlers.py:48 +msgid "SSH Key" +msgstr "SSH 密钥" + +#: audits/signal_handlers.py:50 settings/serializers/auth/sso.py:10 +msgid "SSO" +msgstr "SSO" + +#: audits/signal_handlers.py:51 +msgid "Auth Token" +msgstr "认证令牌" + +#: audits/signal_handlers.py:52 authentication/notifications.py:73 +#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 +#: users/models/user.py:736 +msgid "WeCom" +msgstr "企业微信" + +#: audits/signal_handlers.py:53 authentication/views/feishu.py:145 +#: authentication/views/login.py:85 notifications/backends/__init__.py:14 +#: settings/serializers/auth/feishu.py:10 users/models/user.py:738 +msgid "FeiShu" +msgstr "飞书" + +#: audits/signal_handlers.py:54 authentication/views/dingtalk.py:180 +#: authentication/views/login.py:79 notifications/backends/__init__.py:12 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:737 +msgid "DingTalk" +msgstr "钉钉" + +#: audits/signal_handlers.py:55 authentication/models/temp_token.py:16 +msgid "Temporary token" +msgstr "临时密码" + +#: authentication/api/confirm.py:40 +msgid "This action require verify your MFA" +msgstr "此操作需要验证您的 MFA" + +#: authentication/api/mfa.py:59 +msgid "Current user not support mfa type: {}" +msgstr "当前用户不支持 MFA 类型: {}" + +#: authentication/api/password.py:31 terminal/api/session/session.py:225 +#: users/views/profile/reset.py:44 +msgid "User does not exist: {}" +msgstr "用户不存在: {}" + +#: authentication/api/password.py:31 users/views/profile/reset.py:127 +msgid "No user matched" +msgstr "没有匹配到用户" + +#: authentication/api/password.py:35 +msgid "" +"The user is from {}, please go to the corresponding system to change the " +"password" +msgstr "用户来自 {} 请去相应系统修改密码" + +#: authentication/api/password.py:59 +#: authentication/templates/authentication/login.html:319 +#: users/templates/users/forgot_password.html:27 +#: users/templates/users/forgot_password.html:28 +#: users/templates/users/forgot_password_previewing.html:13 +#: users/templates/users/forgot_password_previewing.html:14 +msgid "Forgot password" +msgstr "忘记密码" + +#: authentication/apps.py:7 settings/serializers/auth/base.py:10 +#: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 +#: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 +#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 +#: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 +msgid "Authentication" +msgstr "认证" + +#: authentication/backends/custom.py:58 +#: authentication/backends/oauth2/backends.py:158 +msgid "User invalid, disabled or expired" +msgstr "用户无效,已禁用或已过期" + +#: authentication/backends/drf.py:56 +msgid "Invalid signature header. No credentials provided." +msgstr "不合法的签名头" + +#: authentication/backends/drf.py:59 +msgid "Invalid signature header. Signature string should not contain spaces." +msgstr "不合法的签名头" + +#: authentication/backends/drf.py:66 +msgid "Invalid signature header. Format like AccessKeyId:Signature" +msgstr "不合法的签名头" + +#: authentication/backends/drf.py:70 +msgid "" +"Invalid signature header. Signature string should not contain invalid " +"characters." +msgstr "不合法的签名头" + +#: authentication/backends/drf.py:90 authentication/backends/drf.py:106 +msgid "Invalid signature." +msgstr "签名无效" + +#: authentication/backends/drf.py:97 +msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" +msgstr "HTTP header not valid" + +#: authentication/backends/drf.py:102 +msgid "Expired, more than 15 minutes" +msgstr "已过期,超过15分钟" + +#: authentication/backends/drf.py:109 +msgid "User disabled." +msgstr "用户已禁用" + +#: authentication/backends/drf.py:127 +msgid "Invalid token header. No credentials provided." +msgstr "无效的令牌头。没有提供任何凭据。" + +#: authentication/backends/drf.py:130 +msgid "Invalid token header. Sign string should not contain spaces." +msgstr "无效的令牌头。符号字符串不应包含空格。" + +#: authentication/backends/drf.py:137 +msgid "" +"Invalid token header. Sign string should not contain invalid characters." +msgstr "无效的令牌头。符号字符串不应包含无效字符。" + +#: authentication/backends/drf.py:148 +msgid "Invalid token or cache refreshed." +msgstr "刷新的令牌或缓存无效。" + +#: authentication/confirm/password.py:16 +msgid "Authentication failed password incorrect" +msgstr "认证失败 (用户名或密码不正确)" + +#: authentication/confirm/relogin.py:10 +msgid "Login time has exceeded {} minutes, please login again" +msgstr "登录时长已超过 {} 分钟,请重新登录" + +#: authentication/errors/const.py:18 +msgid "Username/password check failed" +msgstr "用户名/密码 校验失败" + +#: authentication/errors/const.py:19 +msgid "Password decrypt failed" +msgstr "密码解密失败" + +#: authentication/errors/const.py:20 +msgid "MFA failed" +msgstr "MFA 校验失败" + +#: authentication/errors/const.py:21 +msgid "MFA unset" +msgstr "MFA 没有设定" + +#: authentication/errors/const.py:22 +msgid "Username does not exist" +msgstr "用户名不存在" + +#: authentication/errors/const.py:23 +msgid "Password expired" +msgstr "密码已过期" + +#: authentication/errors/const.py:24 +msgid "Disabled or expired" +msgstr "禁用或失效" + +#: authentication/errors/const.py:25 +msgid "This account is inactive." +msgstr "此账号已禁用" + +#: authentication/errors/const.py:26 +msgid "This account is expired" +msgstr "此账号已过期" + +#: authentication/errors/const.py:27 +msgid "Auth backend not match" +msgstr "没有匹配到认证后端" + +#: authentication/errors/const.py:28 +msgid "ACL is not allowed" +msgstr "登录访问控制不被允许" + +#: authentication/errors/const.py:29 +msgid "Only local users are allowed" +msgstr "仅允许本地用户" + +#: authentication/errors/const.py:39 +msgid "No session found, check your cookie" +msgstr "会话已变更,刷新页面" + +#: authentication/errors/const.py:41 +#, python-brace-format +msgid "" +"The username or password you entered is incorrect, please enter it again. " +"You can also try {times_try} times (The account will be temporarily locked " +"for {block_time} minutes)" +msgstr "" +"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" +"被临时 锁定 {block_time} 分钟)" + +#: authentication/errors/const.py:47 authentication/errors/const.py:55 +msgid "" +"The account has been locked (please contact admin to unlock it or try again " +"after {} minutes)" +msgstr "账号已被锁定(请联系管理员解锁或{}分钟后重试)" + +#: authentication/errors/const.py:51 +msgid "" +"The address has been locked (please contact admin to unlock it or try again " +"after {} minutes)" +msgstr "IP 已被锁定(请联系管理员解锁或 {} 分钟后重试)" + +#: authentication/errors/const.py:59 +#, python-brace-format +msgid "" +"{error}, You can also try {times_try} times (The account will be temporarily " +"locked for {block_time} minutes)" +msgstr "" +"{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" + +#: authentication/errors/const.py:63 +msgid "MFA required" +msgstr "需要 MFA 认证" + +#: authentication/errors/const.py:64 +msgid "MFA not set, please set it first" +msgstr "MFA 没有设置,请先完成设置" + +#: authentication/errors/const.py:65 +msgid "Login confirm required" +msgstr "需要登录复核" + +#: authentication/errors/const.py:66 +msgid "Wait login confirm ticket for accept" +msgstr "等待登录复核处理" + +#: authentication/errors/const.py:67 +msgid "Login confirm ticket was {}" +msgstr "登录复核: {}" + +#: authentication/errors/failed.py:146 +msgid "Current IP and Time period is not allowed" +msgstr "当前 IP 和时间段不被允许登录" + +#: authentication/errors/failed.py:151 +msgid "Please enter MFA code" +msgstr "请输入 MFA 验证码" + +#: authentication/errors/failed.py:156 +msgid "Please enter SMS code" +msgstr "请输入短信验证码" + +#: authentication/errors/failed.py:161 users/exceptions.py:15 +msgid "Phone not set" +msgstr "手机号没有设置" + +#: authentication/errors/mfa.py:8 +msgid "SSO auth closed" +msgstr "SSO 认证关闭了" + +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80 +msgid "WeCom is already bound" +msgstr "企业微信已经绑定" + +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237 +#: authentication/views/wecom.py:291 +msgid "WeCom is not bound" +msgstr "没有绑定企业微信" + +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 +#: authentication/views/dingtalk.py:297 +msgid "DingTalk is not bound" +msgstr "钉钉没有绑定" + +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 +msgid "FeiShu is not bound" +msgstr "没有绑定飞书" + +#: authentication/errors/mfa.py:38 +msgid "Your password is invalid" +msgstr "您的密码无效" + +#: authentication/errors/redirect.py:85 authentication/mixins.py:306 +msgid "Your password is too simple, please change it for security" +msgstr "你的密码过于简单,为了安全,请修改" + +#: authentication/errors/redirect.py:93 authentication/mixins.py:313 +msgid "You should to change your password before login" +msgstr "登录完成前,请先修改密码" + +#: authentication/errors/redirect.py:101 authentication/mixins.py:320 +msgid "Your password has expired, please reset before logging in" +msgstr "您的密码已过期,先修改再登录" + +#: authentication/forms.py:45 +msgid "{} days auto login" +msgstr "{} 天内自动登录" + +#: authentication/forms.py:56 +msgid "MFA Code" +msgstr "MFA 验证码" + +#: authentication/forms.py:57 +msgid "MFA type" +msgstr "MFA 类型" + +#: authentication/forms.py:65 +#: authentication/templates/authentication/_captcha_field.html:15 +msgid "Captcha" +msgstr "验证码" + +#: authentication/forms.py:70 users/forms/profile.py:28 +msgid "MFA code" +msgstr "MFA 验证码" + +#: authentication/forms.py:72 +msgid "Dynamic code" +msgstr "动态码" + +#: authentication/mfa/base.py:7 +msgid "Please input security code" +msgstr "请输入动态安全码" + +#: authentication/mfa/custom.py:20 +msgid "MFA Custom code invalid" +msgstr "自定义 MFA 验证码校验失败" + +#: authentication/mfa/custom.py:26 +msgid "MFA custom verification code" +msgstr "自定义 MFA 验证码" + +#: authentication/mfa/custom.py:56 +msgid "MFA custom global enabled, cannot disable" +msgstr "自定义 MFA 全局开启,无法被禁用" + +#: authentication/mfa/otp.py:7 +msgid "OTP code invalid, or server time error" +msgstr "虚拟 MFA 验证码错误,或者服务器端时间不对" + +#: authentication/mfa/otp.py:12 +msgid "OTP" +msgstr "虚拟 MFA" + +#: authentication/mfa/otp.py:13 +msgid "OTP verification code" +msgstr "虚拟 MFA 验证码" + +#: authentication/mfa/otp.py:48 +msgid "Virtual OTP based MFA" +msgstr "虚拟 MFA(OTP)" + +#: authentication/mfa/radius.py:7 +msgid "Radius verify code invalid" +msgstr "Radius 校验失败" + +#: authentication/mfa/radius.py:13 +msgid "Radius verification code" +msgstr "Radius 动态安全码" + +#: authentication/mfa/radius.py:44 +msgid "Radius global enabled, cannot disable" +msgstr "Radius MFA 全局开启,无法被禁用" + +#: authentication/mfa/sms.py:7 +msgid "SMS verify code invalid" +msgstr "短信验证码校验失败" + +#: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16 +#: authentication/serializers/password_mfa.py:24 +#: settings/serializers/auth/sms.py:27 users/forms/profile.py:103 +#: users/forms/profile.py:106 users/templates/users/forgot_password.html:111 +#: users/views/profile/reset.py:79 +msgid "SMS" +msgstr "短信" + +#: authentication/mfa/sms.py:13 +msgid "SMS verification code" +msgstr "短信验证码" + +#: authentication/mfa/sms.py:57 +msgid "Set phone number to enable" +msgstr "设置手机号码启用" + +#: authentication/mfa/sms.py:61 +msgid "Clear phone number to disable" +msgstr "清空手机号码禁用" + +#: authentication/middleware.py:77 settings/utils/ldap.py:652 +msgid "Authentication failed (before login check failed): {}" +msgstr "认证失败(登录前检查失败): {}" + +#: authentication/mixins.py:256 +msgid "The MFA type ({}) is not enabled" +msgstr "该 MFA ({}) 方式没有启用" + +#: authentication/mixins.py:296 +msgid "Please change your password" +msgstr "请修改密码" + +#: authentication/models/connection_token.py:34 +#: terminal/serializers/storage.py:111 +msgid "Account name" +msgstr "账号名称" + +#: authentication/models/connection_token.py:35 +msgid "Input username" +msgstr "自定义用户名" + +#: authentication/models/connection_token.py:36 +#: authentication/serializers/connection_token.py:16 +msgid "Input secret" +msgstr "自定义密码" + +#: authentication/models/connection_token.py:38 +msgid "Connect method" +msgstr "连接方式" + +#: authentication/models/connection_token.py:39 +#: rbac/serializers/rolebinding.py:21 +msgid "User display" +msgstr "用户名称" + +#: authentication/models/connection_token.py:40 +msgid "Asset display" +msgstr "资产名称" + +#: authentication/models/connection_token.py:41 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:73 +#: tickets/models/ticket/apply_application.py:31 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:719 +msgid "Date expired" +msgstr "失效日期" + +#: authentication/models/connection_token.py:45 +msgid "Connection token" +msgstr "连接令牌" + +#: authentication/models/connection_token.py:47 +msgid "Can view connection token secret" +msgstr "可以查看连接令牌密文" + +#: authentication/models/connection_token.py:94 +msgid "Connection token expired at: {}" +msgstr "连接令牌过期: {}" + +#: authentication/models/connection_token.py:97 +msgid "No user or invalid user" +msgstr "没有用户或用户失效" + +#: authentication/models/connection_token.py:101 +msgid "No asset or inactive asset" +msgstr "没有资产或资产未激活" + +#: authentication/models/connection_token.py:248 +msgid "Super connection token" +msgstr "超级连接令牌" + +#: authentication/models/private_token.py:9 +msgid "Private Token" +msgstr "私有令牌" + +#: authentication/models/sso_token.py:15 +msgid "Expired" +msgstr "过期时间" + +#: authentication/models/sso_token.py:20 +msgid "SSO token" +msgstr "SSO token" + +#: authentication/models/temp_token.py:11 +msgid "Verified" +msgstr "已校验" + +#: authentication/notifications.py:19 +msgid "Different city login reminder" +msgstr "异地登录提醒" + +#: authentication/notifications.py:52 +msgid "binding reminder" +msgstr "绑定提醒" + +#: authentication/serializers/connect_token_secret.py:105 +msgid "Is builtin" +msgstr "内置的" + +#: authentication/serializers/connect_token_secret.py:109 +msgid "Options" +msgstr "选项" + +#: authentication/serializers/connect_token_secret.py:116 +msgid "Component" +msgstr "组件" + +#: authentication/serializers/connect_token_secret.py:127 +msgid "Expired now" +msgstr "立刻过期" + +#: authentication/serializers/connect_token_secret.py:146 +#: authentication/templates/authentication/_access_key_modal.html:30 +#: perms/models/perm_node.py:20 users/serializers/group.py:35 +msgid "ID" +msgstr "ID" + +#: authentication/serializers/connection_token.py:14 +msgid "Expired time" +msgstr "过期时间" + +#: authentication/serializers/password_mfa.py:16 +#: authentication/serializers/password_mfa.py:24 +#: notifications/backends/__init__.py:10 settings/serializers/email.py:19 +#: settings/serializers/email.py:50 users/forms/profile.py:102 +#: users/forms/profile.py:106 users/models/user.py:677 +#: users/templates/users/forgot_password.html:116 +#: users/views/profile/reset.py:73 +msgid "Email" +msgstr "邮箱" + +#: authentication/serializers/password_mfa.py:29 +#: users/templates/users/forgot_password.html:107 +msgid "The {} cannot be empty" +msgstr "{} 不能为空" + +#: authentication/serializers/token.py:79 perms/serializers/permission.py:30 +#: perms/serializers/permission.py:61 users/serializers/user.py:159 +msgid "Is valid" +msgstr "账号是否有效" + +#: authentication/templates/authentication/_access_key_modal.html:6 +msgid "API key list" +msgstr "API Key列表" + +#: authentication/templates/authentication/_access_key_modal.html:18 +msgid "Using api key sign api header, every requests header difference" +msgstr "使用api key签名请求头,每个请求的头部是不一样的" + +#: authentication/templates/authentication/_access_key_modal.html:19 +msgid "docs" +msgstr "文档" + +#: authentication/templates/authentication/_access_key_modal.html:33 +#: terminal/notifications.py:96 terminal/notifications.py:144 +msgid "Date" +msgstr "日期" + +#: authentication/templates/authentication/_access_key_modal.html:48 +msgid "Show" +msgstr "显示" + +#: authentication/templates/authentication/_access_key_modal.html:66 +#: settings/serializers/security.py:39 users/models/user.py:559 +#: users/serializers/profile.py:116 users/templates/users/mfa_setting.html:61 +#: users/templates/users/user_verify_mfa.html:36 +msgid "Disable" +msgstr "禁用" + +#: authentication/templates/authentication/_access_key_modal.html:67 +#: users/models/user.py:560 users/serializers/profile.py:117 +#: users/templates/users/mfa_setting.html:26 +#: users/templates/users/mfa_setting.html:68 +msgid "Enable" +msgstr "启用" + +#: authentication/templates/authentication/_access_key_modal.html:147 +msgid "Delete success" +msgstr "删除成功" + +#: authentication/templates/authentication/_access_key_modal.html:155 +#: authentication/templates/authentication/_mfa_confirm_modal.html:53 +#: templates/_modal.html:22 tickets/const.py:44 +msgid "Close" +msgstr "关闭" + +#: authentication/templates/authentication/_captcha_field.html:8 +msgid "Play CAPTCHA as audio file" +msgstr "语言播放验证码" + +#: authentication/templates/authentication/_mfa_confirm_modal.html:5 +msgid "MFA confirm" +msgstr "MFA 认证校验" + +#: authentication/templates/authentication/_mfa_confirm_modal.html:17 +msgid "Need MFA for view auth" +msgstr "需要 MFA 认证来查看账号信息" + +#: authentication/templates/authentication/_mfa_confirm_modal.html:20 +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37 +#: templates/_modal.html:23 templates/flash_message_standalone.html:37 +#: users/templates/users/user_password_verify.html:20 +msgid "Confirm" +msgstr "确认" + +#: authentication/templates/authentication/_mfa_confirm_modal.html:25 +msgid "Code error" +msgstr "代码错误" + +#: authentication/templates/authentication/_msg_different_city.html:3 +#: authentication/templates/authentication/_msg_oauth_bind.html:3 +#: authentication/templates/authentication/_msg_reset_password.html:3 +#: authentication/templates/authentication/_msg_reset_password_code.html:9 +#: authentication/templates/authentication/_msg_rest_password_success.html:2 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:2 +#: jumpserver/conf.py:416 +#: perms/templates/perms/_msg_item_permissions_expire.html:3 +#: perms/templates/perms/_msg_permed_items_expire.html:3 +#: tickets/templates/tickets/approve_check_password.html:33 +#: users/templates/users/_msg_account_expire_reminder.html:4 +#: users/templates/users/_msg_password_expire_reminder.html:4 +#: users/templates/users/_msg_reset_mfa.html:4 +#: users/templates/users/_msg_reset_ssh_key.html:4 +msgid "Hello" +msgstr "你好" + +#: authentication/templates/authentication/_msg_different_city.html:6 +msgid "Your account has remote login behavior, please pay attention" +msgstr "你的账号存在异地登录行为,请关注。" + +#: authentication/templates/authentication/_msg_different_city.html:10 +msgid "Login time" +msgstr "登录日期" + +#: authentication/templates/authentication/_msg_different_city.html:16 +msgid "" +"If you suspect that the login behavior is abnormal, please modify the " +"account password in time." +msgstr "若怀疑此次登录行为异常,请及时修改账号密码" + +#: authentication/templates/authentication/_msg_oauth_bind.html:6 +msgid "Your account has just been bound to" +msgstr "您的帐户刚刚绑定到" + +#: authentication/templates/authentication/_msg_oauth_bind.html:17 +msgid "If the operation is not your own, unbind and change the password." +msgstr "如果操作不是您本人,请解绑并且修改密码" + +#: authentication/templates/authentication/_msg_reset_password.html:6 +msgid "" +"Please click the link below to reset your password, if not your request, " +"concern your account security" +msgstr "请点击下面链接重置密码, 如果不是您申请的,请关注账号安全" + +#: authentication/templates/authentication/_msg_reset_password.html:10 +msgid "Click here reset password" +msgstr "点击这里重置密码" + +#: authentication/templates/authentication/_msg_reset_password.html:16 +#: users/templates/users/_msg_user_created.html:22 +msgid "This link is valid for 1 hour. After it expires" +msgstr "这个链接有效期1小时, 超过时间您可以" + +#: authentication/templates/authentication/_msg_reset_password.html:17 +#: users/templates/users/_msg_user_created.html:23 +msgid "request new one" +msgstr "重新申请" + +#: authentication/templates/authentication/_msg_reset_password_code.html:12 +#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:79 +#: users/forms/profile.py:104 users/templates/users/forgot_password.html:65 +msgid "Verify code" +msgstr "验证码" + +#: authentication/templates/authentication/_msg_reset_password_code.html:15 +msgid "" +"Copy the verification code to the Reset Password page to reset the password." +msgstr "将验证码复制到重置密码页面,重置密码。" + +#: authentication/templates/authentication/_msg_reset_password_code.html:18 +msgid "The validity period of the verification code is one minute" +msgstr "验证码有效期为 1 分钟" + +#: authentication/templates/authentication/_msg_rest_password_success.html:5 +msgid "Your password has just been successfully updated" +msgstr "你的密码刚刚成功更新" + +#: authentication/templates/authentication/_msg_rest_password_success.html:9 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:9 +msgid "Browser" +msgstr "浏览器" + +#: authentication/templates/authentication/_msg_rest_password_success.html:13 +msgid "" +"If the password update was not initiated by you, your account may have " +"security issues" +msgstr "如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题" + +#: authentication/templates/authentication/_msg_rest_password_success.html:14 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:14 +msgid "If you have any questions, you can contact the administrator" +msgstr "如果有疑问或需求,请联系系统管理员" + +#: authentication/templates/authentication/_msg_rest_public_key_success.html:5 +msgid "Your public key has just been successfully updated" +msgstr "你的公钥刚刚成功更新" + +#: authentication/templates/authentication/_msg_rest_public_key_success.html:13 +msgid "" +"If the public key update was not initiated by you, your account may have " +"security issues" +msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能存在安全问题" + +#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28 +#: templates/flash_message_standalone.html:28 tickets/const.py:17 +msgid "Cancel" +msgstr "取消" + +#: authentication/templates/authentication/login.html:254 +#: authentication/templates/authentication/login.html:327 +#: templates/_header_bar.html:89 +msgid "Login" +msgstr "登录" + +#: authentication/templates/authentication/login.html:334 +msgid "More login options" +msgstr "其他方式登录" + +#: authentication/templates/authentication/login_mfa.html:6 +msgid "MFA Auth" +msgstr "MFA 多因子认证" + +#: authentication/templates/authentication/login_mfa.html:19 +#: users/templates/users/user_otp_check_password.html:12 +#: users/templates/users/user_otp_enable_bind.html:24 +#: users/templates/users/user_otp_enable_install_app.html:29 +#: users/templates/users/user_verify_mfa.html:30 +msgid "Next" +msgstr "下一步" + +#: authentication/templates/authentication/login_mfa.html:22 +msgid "Can't provide security? Please contact the administrator!" +msgstr "如果不能提供 MFA 验证码,请联系管理员!" + +#: authentication/templates/authentication/login_wait_confirm.html:41 +msgid "Refresh" +msgstr "刷新" + +#: authentication/templates/authentication/login_wait_confirm.html:46 +msgid "Copy link" +msgstr "复制链接" + +#: authentication/templates/authentication/login_wait_confirm.html:51 +msgid "Return" +msgstr "返回" + +#: authentication/templates/authentication/login_wait_confirm.html:116 +msgid "Copy success" +msgstr "复制成功" + +#: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 +#: xpack/plugins/cloud/const.py:27 +msgid "LAN" +msgstr "局域网" + +#: authentication/views/dingtalk.py:42 +msgid "DingTalk Error, Please contact your system administrator" +msgstr "钉钉错误,请联系系统管理员" + +#: authentication/views/dingtalk.py:45 +msgid "DingTalk Error" +msgstr "钉钉错误" + +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 +#: authentication/views/wecom.py:56 +msgid "" +"The system configuration is incorrect. Please contact your administrator" +msgstr "企业配置错误,请联系系统管理员" + +#: authentication/views/dingtalk.py:81 +msgid "DingTalk is already bound" +msgstr "钉钉已经绑定" + +#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 +msgid "Invalid user_id" +msgstr "无效的 user_id" + +#: authentication/views/dingtalk.py:165 +msgid "DingTalk query user failed" +msgstr "钉钉查询用户失败" + +#: authentication/views/dingtalk.py:174 +msgid "The DingTalk is already bound to another user" +msgstr "该钉钉已经绑定其他用户" + +#: authentication/views/dingtalk.py:181 +msgid "Binding DingTalk successfully" +msgstr "绑定 钉钉 成功" + +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 +msgid "Failed to get user from DingTalk" +msgstr "从钉钉获取用户失败" + +#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 +msgid "Please login with a password and then bind the DingTalk" +msgstr "请使用密码登录,然后绑定钉钉" + +#: authentication/views/feishu.py:40 +msgid "FeiShu Error" +msgstr "飞书错误" + +#: authentication/views/feishu.py:88 +msgid "FeiShu is already bound" +msgstr "飞书已经绑定" + +#: authentication/views/feishu.py:130 +msgid "FeiShu query user failed" +msgstr "飞书查询用户失败" + +#: authentication/views/feishu.py:139 +msgid "The FeiShu is already bound to another user" +msgstr "该飞书已经绑定其他用户" + +#: authentication/views/feishu.py:146 +msgid "Binding FeiShu successfully" +msgstr "绑定 飞书 成功" + +#: authentication/views/feishu.py:198 +msgid "Failed to get user from FeiShu" +msgstr "从飞书获取用户失败" + +#: authentication/views/feishu.py:205 +msgid "Please login with a password and then bind the FeiShu" +msgstr "请使用密码登录,然后绑定飞书" + +#: authentication/views/login.py:181 +msgid "Redirecting" +msgstr "跳转中" + +#: authentication/views/login.py:182 +msgid "Redirecting to {} authentication" +msgstr "正在跳转到 {} 认证" + +#: authentication/views/login.py:205 +msgid "Please enable cookies and try again." +msgstr "设置你的浏览器支持cookie" + +#: authentication/views/login.py:307 +msgid "" +"Wait for {} confirm, You also can copy link to her/him
\n" +" Don't close this page" +msgstr "" +"等待 {} 确认, 你也可以复制链接发给他/她
\n" +" 不要关闭本页面" + +#: authentication/views/login.py:312 +msgid "No ticket found" +msgstr "没有发现工单" + +#: authentication/views/login.py:348 +msgid "Logout success" +msgstr "退出登录成功" + +#: authentication/views/login.py:349 +msgid "Logout success, return login page" +msgstr "退出登录成功,返回到登录页面" + +#: authentication/views/wecom.py:41 +msgid "WeCom Error, Please contact your system administrator" +msgstr "企业微信错误,请联系系统管理员" + +#: authentication/views/wecom.py:44 +msgid "WeCom Error" +msgstr "企业微信错误" + +#: authentication/views/wecom.py:163 +msgid "WeCom query user failed" +msgstr "企业微信查询用户失败" + +#: authentication/views/wecom.py:172 +msgid "The WeCom is already bound to another user" +msgstr "该企业微信已经绑定其他用户" + +#: authentication/views/wecom.py:179 +msgid "Binding WeCom successfully" +msgstr "绑定 企业微信 成功" + +#: authentication/views/wecom.py:231 authentication/views/wecom.py:285 +msgid "Failed to get user from WeCom" +msgstr "从企业微信获取用户失败" + +#: authentication/views/wecom.py:238 authentication/views/wecom.py:292 +msgid "Please login with a password and then bind the WeCom" +msgstr "请使用密码登录,然后绑定企业微信" + +#: common/const/__init__.py:6 +#, python-format +msgid "%(name)s was created successfully" +msgstr "%(name)s 创建成功" + +#: common/const/__init__.py:7 +#, python-format +msgid "%(name)s was updated successfully" +msgstr "%(name)s 更新成功" + +#: common/const/choices.py:10 +msgid "Manual trigger" +msgstr "手动触发" + +#: common/const/choices.py:11 +msgid "Timing trigger" +msgstr "定时触发" + +#: common/const/choices.py:15 +msgid "Ready" +msgstr "准备" + +#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:39 +msgid "Pending" +msgstr "待定的" + +#: common/const/choices.py:17 ops/const.py:49 +msgid "Running" +msgstr "运行中" + +#: common/const/choices.py:21 +msgid "Canceled" +msgstr "取消" + +#: common/db/encoder.py:11 +msgid "ugettext_lazy" +msgstr "ugettext_lazy" + +#: common/db/fields.py:94 +msgid "Marshal dict data to char field" +msgstr "编码 dict 为 char" + +#: common/db/fields.py:98 +msgid "Marshal dict data to text field" +msgstr "编码 dict 为 text" + +#: common/db/fields.py:110 +msgid "Marshal list data to char field" +msgstr "编码 list 为 char" + +#: common/db/fields.py:114 +msgid "Marshal list data to text field" +msgstr "编码 list 为 text" + +#: common/db/fields.py:118 +msgid "Marshal data to char field" +msgstr "编码数据为 char" + +#: common/db/fields.py:122 +msgid "Marshal data to text field" +msgstr "编码数据为 text" + +#: common/db/fields.py:164 +msgid "Encrypt field using Secret Key" +msgstr "加密的字段" + +#: common/db/models.py:75 +msgid "Updated by" +msgstr "更新人" + +#: common/drf/exc_handlers.py:25 +msgid "Object" +msgstr "对象" + +#: common/drf/fields.py:77 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "该字段是必填项。" + +#: common/drf/fields.py:78 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "错误的 id \"{pk_value}\" - 对象不存在" + +#: common/drf/fields.py:79 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "错误类型。期望 pk 值,收到 {data_type}。" + +#: common/drf/fields.py:141 +msgid "Invalid data type, should be list" +msgstr "错误的数据类型,应该是列表" + +#: common/drf/fields.py:156 +msgid "Invalid choice: {}" +msgstr "无效选项: {}" + +#: common/drf/metadata.py:130 +msgid "Organization ID" +msgstr "组织 ID" + +#: common/drf/parsers/base.py:17 +msgid "The file content overflowed (The maximum length `{}` bytes)" +msgstr "文件内容太大 (最大长度 `{}` 字节)" + +#: common/drf/parsers/base.py:159 +msgid "Parse file error: {}" +msgstr "解析文件错误: {}" + +#: common/drf/serializers/common.py:86 +msgid "Children" +msgstr "节点" + +#: common/drf/serializers/common.py:94 +msgid "File" +msgstr "文件" + +#: common/exceptions.py:15 +#, python-format +msgid "%s object does not exist." +msgstr "%s对象不存在" + +#: common/exceptions.py:25 +msgid "Someone else is doing this. Please wait for complete" +msgstr "其他人正在操作,请等待他人完成" + +#: common/exceptions.py:30 +msgid "Your request timeout" +msgstr "您的请求超时了" + +#: common/exceptions.py:35 +msgid "M2M reverse not allowed" +msgstr "多对多反向是不被允许的" + +#: common/exceptions.py:41 +msgid "Is referenced by other objects and cannot be deleted" +msgstr "被其他对象关联,不能删除" + +#: common/exceptions.py:48 +msgid "This action require confirm current user" +msgstr "此操作需要确认当前用户" + +#: common/exceptions.py:56 +msgid "Unexpect error occur" +msgstr "发生意外错误" + +#: common/mixins/api/action.py:52 +msgid "Request file format may be wrong" +msgstr "上传的文件格式错误 或 其它类型资源的文件" + +#: common/mixins/models.py:32 +msgid "is discard" +msgstr "忽略的" + +#: common/mixins/models.py:33 +msgid "discard time" +msgstr "忽略时间" + +#: common/mixins/views.py:58 +msgid "Export all" +msgstr "导出所有" + +#: common/mixins/views.py:60 +msgid "Export only selected items" +msgstr "仅导出选择项" + +#: common/mixins/views.py:65 +#, python-format +msgid "Export filtered: %s" +msgstr "导出搜素: %s" + +#: common/plugins/es.py:28 +msgid "Invalid elasticsearch config" +msgstr "无效的 Elasticsearch 配置" + +#: common/plugins/es.py:33 +msgid "Not Support Elasticsearch8" +msgstr "不支持 Elasticsearch8" + +#: common/sdk/im/exceptions.py:23 +msgid "Network error, please contact system administrator" +msgstr "网络错误,请联系系统管理员" + +#: common/sdk/im/wecom/__init__.py:15 +msgid "WeCom error, please contact system administrator" +msgstr "企业微信错误,请联系系统管理员" + +#: common/sdk/sms/alibaba.py:56 +msgid "Signature does not match" +msgstr "签名不匹配" + +#: common/sdk/sms/cmpp2.py:46 +msgid "sp_id is 6 bits" +msgstr "SP_id 为6位" + +#: common/sdk/sms/cmpp2.py:216 +msgid "Failed to connect to the CMPP gateway server, err: {}" +msgstr "连接网关服务器错误,错误:{}" + +#: common/sdk/sms/endpoint.py:16 +msgid "Alibaba cloud" +msgstr "阿里云" + +#: common/sdk/sms/endpoint.py:17 +msgid "Tencent cloud" +msgstr "腾讯云" + +#: common/sdk/sms/endpoint.py:18 xpack/plugins/cloud/const.py:13 +msgid "Huawei Cloud" +msgstr "华为云" + +#: common/sdk/sms/endpoint.py:19 +msgid "CMPP v2.0" +msgstr "CMPP v2.0" + +#: common/sdk/sms/endpoint.py:30 +msgid "SMS provider not support: {}" +msgstr "短信服务商不支持:{}" + +#: common/sdk/sms/endpoint.py:51 +msgid "SMS verification code signature or template invalid" +msgstr "短信验证码签名或模版无效" + +#: common/sdk/sms/exceptions.py:8 +msgid "The verification code has expired. Please resend it" +msgstr "验证码已过期,请重新发送" + +#: common/sdk/sms/exceptions.py:13 +msgid "The verification code is incorrect" +msgstr "验证码错误" + +#: common/sdk/sms/exceptions.py:18 +msgid "Please wait {} seconds before sending" +msgstr "请在 {} 秒后发送" + +#: common/tasks.py:13 +msgid "Send email" +msgstr "发件邮件" + +#: common/tasks.py:40 +msgid "Send email attachment" +msgstr "发送邮件附件" + +#: common/utils/ip/geoip/utils.py:26 +msgid "Invalid ip" +msgstr "无效IP" + +#: common/utils/ip/utils.py:78 +msgid "Invalid address" +msgstr "无效地址" + +#: common/validators.py:14 +msgid "Special char not allowed" +msgstr "不能包含特殊字符" + +#: common/validators.py:32 +msgid "This field must be unique." +msgstr "字段必须唯一" + +#: common/validators.py:40 +msgid "Should not contains special characters" +msgstr "不能包含特殊字符" + +#: common/validators.py:46 +msgid "The mobile phone number format is incorrect" +msgstr "手机号格式不正确" + +#: jumpserver/conf.py:415 +msgid "Create account successfully" +msgstr "创建账号成功" + +#: jumpserver/conf.py:417 +msgid "Your account has been created successfully" +msgstr "你的账号已创建成功" + +#: jumpserver/context_processor.py:12 +msgid "JumpServer Open Source Bastion Host" +msgstr "JumpServer 开源堡垒机" + +#: jumpserver/views/celery_flower.py:23 +msgid "

Flower service unavailable, check it

" +msgstr "Flower 服务不可用,请检查" + +#: jumpserver/views/other.py:26 +msgid "" +"
Luna is a separately deployed program, you need to deploy Luna, koko, " +"configure nginx for url distribution,
If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck." +msgstr "" +"
Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了" +"这个页面,证明你访问的不是nginx监听的端口,祝你好运
" + +#: jumpserver/views/other.py:70 +msgid "Websocket server run on port: {}, you should proxy it on nginx" +msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" + +#: jumpserver/views/other.py:84 +msgid "" +"
Koko is a separately deployed program, you need to deploy Koko, " +"configure nginx for url distribution,
If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck." +msgstr "" +"
Koko是单独部署的一个程序,你需要部署Koko, 并确保nginx配置转发,
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" + +#: notifications/apps.py:7 +msgid "Notifications" +msgstr "通知" + +#: notifications/backends/__init__.py:13 +msgid "Site message" +msgstr "站内信" + +#: notifications/models/notification.py:14 +msgid "receive backend" +msgstr "消息后端" + +#: notifications/models/notification.py:18 +msgid "User message" +msgstr "用户消息" + +#: notifications/models/notification.py:21 +msgid "{} subscription" +msgstr "{} 订阅" + +#: notifications/models/notification.py:34 +msgid "System message" +msgstr "系统信息" + +#: notifications/notifications.py:46 +msgid "Publish the station message" +msgstr "" + +#: ops/ansible/inventory.py:76 +msgid "No account available" +msgstr "无可用账号" + +#: ops/ansible/inventory.py:180 +msgid "Ansible disabled" +msgstr "Ansible 已禁用" + +#: ops/ansible/inventory.py:196 +msgid "Skip hosts below:" +msgstr "跳过以下主机: " + +#: ops/api/celery.py:63 ops/api/celery.py:78 +msgid "Waiting task start" +msgstr "等待任务开始" + +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +msgid "App ops" +msgstr "作业中心" + +#: ops/const.py:6 +msgid "Push" +msgstr "推送" + +#: ops/const.py:7 +msgid "Verify" +msgstr "校验" + +#: ops/const.py:8 +msgid "Collect" +msgstr "收集" + +#: ops/const.py:9 +msgid "Change password" +msgstr "改密" + +#: ops/const.py:19 +msgid "Custom password" +msgstr "自定义密码" + +#: ops/const.py:33 +msgid "Adhoc" +msgstr "命令" + +#: ops/const.py:34 ops/models/job.py:31 +msgid "Playbook" +msgstr "Playbook" + +#: ops/const.py:38 +msgid "Privileged Only" +msgstr "仅限特权账号" + +#: ops/const.py:39 +msgid "Privileged First" +msgstr "特权账号优先" + +#: ops/const.py:40 +msgid "Skip" +msgstr "跳过" + +#: ops/const.py:45 ops/models/adhoc.py:20 +msgid "Powershell" +msgstr "PowerShell" + +#: ops/exception.py:6 +msgid "no valid program entry found." +msgstr "没有可用程序入口" + +#: ops/mixin.py:25 ops/mixin.py:88 settings/serializers/auth/ldap.py:73 +msgid "Cycle perform" +msgstr "周期执行" + +#: ops/mixin.py:29 ops/mixin.py:86 ops/mixin.py:105 +#: settings/serializers/auth/ldap.py:70 +msgid "Regularly perform" +msgstr "定期执行" + +#: ops/mixin.py:108 +msgid "Interval" +msgstr "间隔" + +#: ops/mixin.py:118 +msgid "* Please enter a valid crontab expression" +msgstr "* 请输入有效的 crontab 表达式" + +#: ops/mixin.py:125 +msgid "Range {} to {}" +msgstr "输入在 {} - {} 范围之间" + +#: ops/mixin.py:136 +msgid "Require periodic or regularly perform setting" +msgstr "需要周期或定期设置" + +#: ops/models/adhoc.py:24 +msgid "Pattern" +msgstr "模式" + +#: ops/models/adhoc.py:26 ops/models/job.py:28 +msgid "Module" +msgstr "模块" + +#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: terminal/models/component/task.py:16 +msgid "Args" +msgstr "参数" + +#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: terminal/models/session/sharing.py:23 +msgid "Creator" +msgstr "创建者" + +#: ops/models/adhoc.py:46 +msgid "AdHoc" +msgstr "任务各版本" + +#: ops/models/base.py:19 +msgid "Account policy" +msgstr "账号策略" + +#: ops/models/base.py:20 +msgid "Last execution" +msgstr "最后执行" + +#: ops/models/base.py:22 +msgid "Date last run" +msgstr "最后运行日期" + +#: ops/models/base.py:51 ops/models/job.py:102 +#: xpack/plugins/cloud/models.py:170 +msgid "Result" +msgstr "结果" + +#: ops/models/base.py:52 ops/models/job.py:103 +msgid "Summary" +msgstr "汇总" + +#: ops/models/celery.py:47 +msgid "Celery Task" +msgstr "Celery 任务" + +#: ops/models/celery.py:56 terminal/models/component/task.py:17 +msgid "Kwargs" +msgstr "其它参数" + +#: ops/models/celery.py:57 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: tickets/serializers/ticket/ticket.py:21 +msgid "State" +msgstr "状态" + +#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: tickets/const.py:25 +msgid "Finished" +msgstr "结束" + +#: ops/models/celery.py:59 +msgid "Date published" +msgstr "发布日期" + +#: ops/models/celery.py:83 +msgid "Celery Task Execution" +msgstr "Celery 任务执行" + +#: ops/models/job.py:29 +msgid "Chdir" +msgstr "运行目录" + +#: ops/models/job.py:30 +msgid "Timeout (Seconds)" +msgstr "超市时间(秒)" + +#: ops/models/job.py:35 +msgid "Runas" +msgstr "运行用户" + +#: ops/models/job.py:37 +msgid "Runas policy" +msgstr "用户策略" + +#: ops/models/job.py:38 +msgid "Use Parameter Define" +msgstr "" + +#: ops/models/job.py:39 +msgid "Parameters define" +msgstr "" + +#: ops/models/job.py:91 +msgid "Job" +msgstr "作业" + +#: ops/models/job.py:101 +msgid "Parameters" +msgstr "" + +#: ops/models/job.py:300 +msgid "Job Execution" +msgstr "作业执行" + +#: ops/models/job.py:311 +msgid "Job audit log" +msgstr "作业审计日志" + +#: ops/notifications.py:17 +msgid "Server performance" +msgstr "监控告警" + +#: ops/notifications.py:23 +msgid "Terminal health check warning" +msgstr "终端健康状况检查警告" + +#: ops/notifications.py:68 +#, python-brace-format +msgid "The terminal is offline: {name}" +msgstr "终端已离线: {name}" + +#: ops/notifications.py:73 +#, python-brace-format +msgid "Disk used more than {max_threshold}%: => {value}" +msgstr "硬盘使用率超过 {max_threshold}%: => {value}" + +#: ops/notifications.py:78 +#, python-brace-format +msgid "Memory used more than {max_threshold}%: => {value}" +msgstr "内存使用率超过 {max_threshold}%: => {value}" + +#: ops/notifications.py:83 +#, python-brace-format +msgid "CPU load more than {max_threshold}: => {value}" +msgstr "CPU 使用率超过 {max_threshold}: => {value}" + +#: ops/serializers/job.py:14 +msgid "Run after save" +msgstr "保存后执行" + +#: ops/serializers/job.py:43 +msgid "Job type" +msgstr "任务类型" + +#: ops/serializers/job.py:44 +msgid "Material" +msgstr "" + +#: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 +#: terminal/models/component/task.py:24 +msgid "Task" +msgstr "任务" + +#: ops/tasks.py:28 +msgid "Run ansible task" +msgstr "运行 Ansible 任务" + +#: ops/tasks.py:35 +msgid "Run ansible task execution" +msgstr "开始执行 Ansible 任务" + +#: ops/tasks.py:49 +msgid "Periodic clear celery tasks" +msgstr "周期清理不可用任务" + +#: ops/tasks.py:51 +msgid "Clean celery log period" +msgstr "定期清除任务日志" + +#: ops/tasks.py:68 +msgid "Clear celery periodic tasks" +msgstr "清理周期任务" + +#: ops/tasks.py:91 +msgid "Create or update periodic tasks" +msgstr "创建或更新周期任务" + +#: ops/tasks.py:99 +msgid "Periodic check service performance" +msgstr "周期检测服务性能" + +#: ops/templates/ops/celery_task_log.html:4 +msgid "Task log" +msgstr "任务列表" + +#: ops/utils.py:64 +msgid "Update task content: {}" +msgstr "更新任务内容: {}" + +#: ops/variables.py:24 +msgid "The current user`s username of JumpServer" +msgstr "" + +#: ops/variables.py:25 +msgid "The id of the asset in the JumpServer" +msgstr "" + +#: ops/variables.py:26 +msgid "The type of the asset in the JumpServer" +msgstr "" + +#: ops/variables.py:27 +msgid "The category of the asset in the JumpServer" +msgstr "" + +#: ops/variables.py:28 +msgid "The name of the asset in the JumpServer" +msgstr "" + +#: ops/variables.py:29 +msgid "Address used to connect this asset in JumpServer" +msgstr "" + +#: ops/variables.py:30 +msgid "Port used to connect this asset in JumpServer" +msgstr "" + +#: ops/variables.py:31 +msgid "ID of the job" +msgstr "" + +#: ops/variables.py:32 +msgid "Name of the job" +msgstr "" + +#: orgs/api.py:67 +msgid "The current organization ({}) cannot be deleted" +msgstr "当前组织 ({}) 不能被删除" + +#: orgs/api.py:72 +msgid "" +"LDAP synchronization is set to the current organization. Please switch to " +"another organization before deleting" +msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进行删除操作" + +#: orgs/api.py:81 +msgid "The organization have resource ({}) cannot be deleted" +msgstr "组织存在资源 ({}) 不能被删除" + +#: orgs/apps.py:7 rbac/tree.py:119 +msgid "App organizations" +msgstr "组织管理" + +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: rbac/const.py:7 rbac/models/rolebinding.py:48 +#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +msgid "Organization" +msgstr "组织" + +#: orgs/mixins/serializers.py:26 rbac/serializers/rolebinding.py:23 +msgid "Org name" +msgstr "组织名称" + +#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +msgid "Builtin" +msgstr "内置的" + +#: orgs/models.py:74 +msgid "GLOBAL" +msgstr "全局组织" + +#: orgs/models.py:76 +msgid "DEFAULT" +msgstr "默认组织" + +#: orgs/models.py:78 +msgid "SYSTEM" +msgstr "系统组织" + +#: orgs/models.py:84 +msgid "Can view root org" +msgstr "可以查看全局组织" + +#: orgs/models.py:85 +msgid "Can view all joined org" +msgstr "可以查看所有加入的组织" + +#: orgs/tasks.py:9 +msgid "Refresh organization cache" +msgstr "刷新组织缓存" + +#: perms/apps.py:9 +msgid "App permissions" +msgstr "授权管理" + +#: perms/const.py:12 +msgid "Connect" +msgstr "连接" + +#: perms/const.py:15 +msgid "Copy" +msgstr "复制" + +#: perms/const.py:16 +msgid "Paste" +msgstr "粘贴" + +#: perms/const.py:26 +msgid "Transfer" +msgstr "文件传输" + +#: perms/const.py:27 +msgid "Clipboard" +msgstr "剪贴板" + +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:59 +#: tickets/models/ticket/apply_application.py:28 +#: tickets/models/ticket/apply_asset.py:18 +msgid "Actions" +msgstr "动作" + +#: perms/models/asset_permission.py:76 +msgid "From ticket" +msgstr "来自工单" + +#: perms/models/asset_permission.py:82 +msgid "Asset permission" +msgstr "资产授权" + +#: perms/models/perm_node.py:67 +msgid "Ungrouped" +msgstr "未分组" + +#: perms/models/perm_node.py:69 +msgid "Favorite" +msgstr "收藏夹" + +#: perms/models/perm_node.py:120 +msgid "Permed asset" +msgstr "授权的资产" + +#: perms/models/perm_node.py:122 +msgid "Can view my assets" +msgstr "可以查看我的资产" + +#: perms/models/perm_node.py:123 +msgid "Can view user assets" +msgstr "可以查看用户授权的资产" + +#: perms/models/perm_node.py:124 +msgid "Can view usergroup assets" +msgstr "可以查看用户组授权的资产" + +#: perms/models/perm_node.py:135 +msgid "Permed account" +msgstr "授权账号" + +#: perms/notifications.py:12 perms/notifications.py:44 +msgid "today" +msgstr "今天" + +#: perms/notifications.py:15 +msgid "You permed assets is about to expire" +msgstr "你授权的资产即将到期" + +#: perms/notifications.py:20 +msgid "permed assets" +msgstr "授权的资产" + +#: perms/notifications.py:59 +msgid "Asset permissions is about to expire" +msgstr "资产授权规则将要过期" + +#: perms/notifications.py:64 +msgid "asset permissions of organization {}" +msgstr "组织 ({}) 的资产授权" + +#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: users/serializers/user.py:91 users/serializers/user.py:161 +msgid "Is expired" +msgstr "已过期" + +#: perms/templates/perms/_msg_item_permissions_expire.html:7 +#: perms/templates/perms/_msg_permed_items_expire.html:7 +#, python-format +msgid "" +"\n" +" The following %(item_type)s will expire in %(count)s days\n" +" " +msgstr "" +"\n" +" 以下 %(item_type)s 即将在 %(count)s 天后过期\n" +" " + +#: perms/templates/perms/_msg_permed_items_expire.html:21 +msgid "If you have any question, please contact the administrator" +msgstr "如果有疑问或需求,请联系系统管理员" + +#: rbac/api/role.py:34 +msgid "Internal role, can't be destroy" +msgstr "内部角色,不能删除" + +#: rbac/api/role.py:38 +msgid "The role has been bound to users, can't be destroy" +msgstr "角色已绑定用户,不能删除" + +#: rbac/api/role.py:60 +msgid "Internal role, can't be update" +msgstr "内部角色,不能更新" + +#: rbac/api/rolebinding.py:52 +msgid "{} at least one system role" +msgstr "{} 至少有一个系统角色" + +#: rbac/apps.py:7 +msgid "RBAC" +msgstr "RBAC" + +#: rbac/builtin.py:111 +msgid "SystemAdmin" +msgstr "系统管理员" + +#: rbac/builtin.py:114 +msgid "SystemAuditor" +msgstr "系统审计员" + +#: rbac/builtin.py:117 +msgid "SystemComponent" +msgstr "系统组件" + +#: rbac/builtin.py:123 +msgid "OrgAdmin" +msgstr "组织管理员" + +#: rbac/builtin.py:126 +msgid "OrgAuditor" +msgstr "组织审计员" + +#: rbac/builtin.py:129 +msgid "OrgUser" +msgstr "组织用户" + +#: rbac/models/menu.py:13 +msgid "Menu permission" +msgstr "菜单授权" + +#: rbac/models/menu.py:15 +msgid "Can view console view" +msgstr "可以显示控制台" + +#: rbac/models/menu.py:16 +msgid "Can view audit view" +msgstr "可以显示审计台" + +#: rbac/models/menu.py:17 +msgid "Can view workbench view" +msgstr "可以显示工作台" + +#: rbac/models/menu.py:18 +msgid "Can view web terminal" +msgstr "Web终端" + +#: rbac/models/menu.py:19 +msgid "Can view file manager" +msgstr "文件管理" + +#: rbac/models/permission.py:26 rbac/models/role.py:34 +msgid "Permissions" +msgstr "授权" + +#: rbac/models/role.py:31 rbac/models/rolebinding.py:38 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +msgid "Scope" +msgstr "范围" + +#: rbac/models/role.py:46 rbac/models/rolebinding.py:44 +#: users/models/user.py:685 +msgid "Role" +msgstr "角色" + +#: rbac/models/role.py:144 +msgid "System role" +msgstr "系统角色" + +#: rbac/models/role.py:152 +msgid "Organization role" +msgstr "组织角色" + +#: rbac/models/rolebinding.py:53 +msgid "Role binding" +msgstr "角色绑定" + +#: rbac/models/rolebinding.py:137 +msgid "All organizations" +msgstr "所有组织" + +#: rbac/models/rolebinding.py:166 +msgid "" +"User last role in org, can not be delete, you can remove user from org " +"instead" +msgstr "用户最后一个角色,不能删除,你可以将用户从组织移除" + +#: rbac/models/rolebinding.py:173 +msgid "Organization role binding" +msgstr "组织角色绑定" + +#: rbac/models/rolebinding.py:188 +msgid "System role binding" +msgstr "系统角色绑定" + +#: rbac/serializers/permission.py:26 users/serializers/profile.py:132 +msgid "Perms" +msgstr "权限" + +#: rbac/serializers/role.py:27 users/serializers/group.py:34 +msgid "Users amount" +msgstr "用户数量" + +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +msgid "Display name" +msgstr "显示名称" + +#: rbac/serializers/rolebinding.py:22 +msgid "Role display" +msgstr "角色显示" + +#: rbac/serializers/rolebinding.py:56 +msgid "Has bound this role" +msgstr "已经绑定" + +#: rbac/tree.py:18 rbac/tree.py:19 +msgid "All permissions" +msgstr "所有权限" + +#: rbac/tree.py:25 +msgid "Console view" +msgstr "控制台" + +#: rbac/tree.py:26 +msgid "Workbench view" +msgstr "工作台" + +#: rbac/tree.py:27 +msgid "Audit view" +msgstr "审计台" + +#: rbac/tree.py:28 settings/models.py:159 +msgid "System setting" +msgstr "系统设置" + +#: rbac/tree.py:29 +msgid "Other" +msgstr "其它" + +#: rbac/tree.py:38 +msgid "Session audits" +msgstr "会话审计" + +#: rbac/tree.py:48 +msgid "Cloud import" +msgstr "云同步" + +#: rbac/tree.py:49 +msgid "Backup account" +msgstr "备份账号" + +#: rbac/tree.py:50 +msgid "Gather account" +msgstr "收集账号" + +#: rbac/tree.py:51 +msgid "Asset change auth" +msgstr "资产改密" + +#: rbac/tree.py:52 +msgid "Terminal setting" +msgstr "终端设置" + +#: rbac/tree.py:53 +msgid "Task Center" +msgstr "任务中心" + +#: rbac/tree.py:54 +msgid "My assets" +msgstr "我的资产" + +#: rbac/tree.py:56 terminal/models/applet/applet.py:38 +#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +msgid "Applet" +msgstr "远程应用" + +#: rbac/tree.py:120 +msgid "Ticket comment" +msgstr "工单评论" + +#: rbac/tree.py:121 tickets/models/ticket/general.py:307 +msgid "Ticket" +msgstr "工单管理" + +#: rbac/tree.py:122 +msgid "Common setting" +msgstr "一般设置" + +#: rbac/tree.py:123 +msgid "View permission tree" +msgstr "查看授权树" + +#: rbac/tree.py:124 +msgid "Execute batch command" +msgstr "执行批量命令" + +#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 +#: settings/api/sms.py:148 settings/api/wecom.py:37 +msgid "Test success" +msgstr "测试成功" + +#: settings/api/email.py:20 +msgid "Test mail sent to {}, please check" +msgstr "邮件已经发送{}, 请检查" + +#: settings/api/ldap.py:166 +msgid "Synchronization start, please wait." +msgstr "同步开始,请稍等" + +#: settings/api/ldap.py:170 +msgid "Synchronization is running, please wait." +msgstr "同步正在运行,请稍等" + +#: settings/api/ldap.py:175 +msgid "Synchronization error: {}" +msgstr "同步错误: {}" + +#: settings/api/ldap.py:213 +msgid "Get ldap users is None" +msgstr "获取 LDAP 用户为 None" + +#: settings/api/ldap.py:222 +msgid "Imported {} users successfully (Organization: {})" +msgstr "成功导入 {} 个用户 ( 组织: {} )" + +#: settings/api/sms.py:130 +msgid "Invalid SMS platform" +msgstr "无效的短信平台" + +#: settings/api/sms.py:136 +msgid "test_phone is required" +msgstr "测试手机号 该字段是必填项。" + +#: settings/apps.py:7 +msgid "Settings" +msgstr "系统设置" + +#: settings/models.py:36 +msgid "Encrypted" +msgstr "加密的" + +#: settings/models.py:161 +msgid "Can change email setting" +msgstr "邮件设置" + +#: settings/models.py:162 +msgid "Can change auth setting" +msgstr "认证设置" + +#: settings/models.py:163 +msgid "Can change system msg sub setting" +msgstr "消息订阅设置" + +#: settings/models.py:164 +msgid "Can change sms setting" +msgstr "短信设置" + +#: settings/models.py:165 +msgid "Can change security setting" +msgstr "安全设置" + +#: settings/models.py:166 +msgid "Can change clean setting" +msgstr "定期清理" + +#: settings/models.py:167 +msgid "Can change interface setting" +msgstr "界面设置" + +#: settings/models.py:168 +msgid "Can change license setting" +msgstr "许可证设置" + +#: settings/models.py:169 +msgid "Can change terminal setting" +msgstr "终端设置" + +#: settings/models.py:170 +msgid "Can change other setting" +msgstr "其它设置" + +#: settings/serializers/auth/base.py:12 +msgid "CAS Auth" +msgstr "CAS 认证" + +#: settings/serializers/auth/base.py:13 +msgid "OPENID Auth" +msgstr "OIDC 认证" + +#: settings/serializers/auth/base.py:14 +msgid "RADIUS Auth" +msgstr "RADIUS 认证" + +#: settings/serializers/auth/base.py:15 +msgid "DingTalk Auth" +msgstr "钉钉 认证" + +#: settings/serializers/auth/base.py:16 +msgid "FeiShu Auth" +msgstr "飞书 认证" + +#: settings/serializers/auth/base.py:17 +msgid "WeCom Auth" +msgstr "企业微信 认证" + +#: settings/serializers/auth/base.py:18 +msgid "SSO Auth" +msgstr "SSO 令牌认证" + +#: settings/serializers/auth/base.py:19 +msgid "SAML2 Auth" +msgstr "SAML2 认证" + +#: settings/serializers/auth/base.py:22 settings/serializers/basic.py:38 +msgid "Forgot password url" +msgstr "忘记密码 URL" + +#: settings/serializers/auth/base.py:28 +msgid "Enable login redirect msg" +msgstr "启用登录跳转提示" + +#: settings/serializers/auth/cas.py:10 +msgid "CAS" +msgstr "CAS" + +#: settings/serializers/auth/cas.py:12 +msgid "Enable CAS Auth" +msgstr "启用 CAS 认证" + +#: settings/serializers/auth/cas.py:13 settings/serializers/auth/oidc.py:54 +msgid "Server url" +msgstr "服务端地址" + +#: settings/serializers/auth/cas.py:16 +msgid "Proxy server url" +msgstr "回调地址" + +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/saml2.py:34 +msgid "Logout completely" +msgstr "同步注销" + +#: settings/serializers/auth/cas.py:23 +msgid "Username attr" +msgstr "用户名属性" + +#: settings/serializers/auth/cas.py:26 +msgid "Enable attributes map" +msgstr "启用属性映射" + +#: settings/serializers/auth/cas.py:28 settings/serializers/auth/saml2.py:33 +msgid "Rename attr" +msgstr "映射属性" + +#: settings/serializers/auth/cas.py:29 +msgid "Create user if not" +msgstr "创建用户(如果不存在)" + +#: settings/serializers/auth/dingtalk.py:15 +msgid "Enable DingTalk Auth" +msgstr "启用钉钉认证" + +#: settings/serializers/auth/feishu.py:14 +msgid "Enable FeiShu Auth" +msgstr "启用飞书认证" + +#: settings/serializers/auth/ldap.py:39 +msgid "LDAP" +msgstr "LDAP" + +#: settings/serializers/auth/ldap.py:42 +msgid "LDAP server" +msgstr "LDAP 地址" + +#: settings/serializers/auth/ldap.py:43 +msgid "eg: ldap://localhost:389" +msgstr "如: ldap://localhost:389" + +#: settings/serializers/auth/ldap.py:45 +msgid "Bind DN" +msgstr "绑定 DN" + +#: settings/serializers/auth/ldap.py:50 +msgid "User OU" +msgstr "用户 OU" + +#: settings/serializers/auth/ldap.py:51 +msgid "Use | split multi OUs" +msgstr "多个 OU 使用 | 分割" + +#: settings/serializers/auth/ldap.py:54 +msgid "User search filter" +msgstr "用户过滤器" + +#: settings/serializers/auth/ldap.py:55 +#, python-format +msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" +msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" + +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/oidc.py:37 +msgid "User attr map" +msgstr "用户属性映射" + +#: settings/serializers/auth/ldap.py:59 +msgid "" +"User attr map present how to map LDAP user attr to jumpserver, username,name," +"email is jumpserver attr" +msgstr "" +"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," +"email 是jumpserver的用户需要属性" + +#: settings/serializers/auth/ldap.py:77 +msgid "Connect timeout" +msgstr "连接超时时间" + +#: settings/serializers/auth/ldap.py:79 +msgid "Search paged size" +msgstr "搜索分页数量" + +#: settings/serializers/auth/ldap.py:81 +msgid "Enable LDAP auth" +msgstr "启用 LDAP 认证" + +#: settings/serializers/auth/oauth2.py:19 +msgid "OAuth2" +msgstr "OAuth2" + +#: settings/serializers/auth/oauth2.py:22 +msgid "Enable OAuth2 Auth" +msgstr "启用 OAuth2 认证" + +#: settings/serializers/auth/oauth2.py:25 +msgid "Logo" +msgstr "图标" + +#: settings/serializers/auth/oauth2.py:28 +msgid "Service provider" +msgstr "服务提供商" + +#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +msgid "Client Id" +msgstr "客户端 ID" + +#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: xpack/plugins/cloud/serializers/account_attrs.py:38 +msgid "Client Secret" +msgstr "客户端密钥" + +#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +msgid "Provider auth endpoint" +msgstr "授权端点地址" + +#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +msgid "Provider token endpoint" +msgstr "token 端点地址" + +#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +msgid "Client authentication method" +msgstr "客户端认证方式" + +#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +msgid "Provider userinfo endpoint" +msgstr "用户信息端点地址" + +#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +msgid "Provider end session endpoint" +msgstr "注销会话端点地址" + +#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/saml2.py:35 +msgid "Always update user" +msgstr "总是更新用户信息" + +#: settings/serializers/auth/oidc.py:12 +msgid "OIDC" +msgstr "OIDC" + +#: settings/serializers/auth/oidc.py:16 +msgid "Base site url" +msgstr "JumpServer 地址" + +#: settings/serializers/auth/oidc.py:32 +msgid "Share session" +msgstr "共享会话" + +#: settings/serializers/auth/oidc.py:34 +msgid "Ignore ssl verification" +msgstr "忽略 SSL 证书验证" + +#: settings/serializers/auth/oidc.py:38 +msgid "" +"User attr map present how to map OpenID user attr to jumpserver, username," +"name,email is jumpserver attr" +msgstr "" +"用户属性映射代表怎样将OpenID中用户属性映射到jumpserver用户上,username, name," +"email 是jumpserver的用户需要属性" + +#: settings/serializers/auth/oidc.py:41 +msgid "Enable PKCE" +msgstr "启用 PKCE" + +#: settings/serializers/auth/oidc.py:43 +msgid "Code challenge method" +msgstr "连接方式" + +#: settings/serializers/auth/oidc.py:51 +msgid "Use Keycloak" +msgstr "使用 Keycloak" + +#: settings/serializers/auth/oidc.py:57 +msgid "Realm name" +msgstr "域" + +#: settings/serializers/auth/oidc.py:63 +msgid "Enable OPENID Auth" +msgstr "启用 OIDC 认证" + +#: settings/serializers/auth/oidc.py:65 +msgid "Provider endpoint" +msgstr "端点地址" + +#: settings/serializers/auth/oidc.py:74 +msgid "Provider jwks endpoint" +msgstr "jwks 端点地址" + +#: settings/serializers/auth/oidc.py:83 +msgid "Provider sign alg" +msgstr "签名算法" + +#: settings/serializers/auth/oidc.py:86 +msgid "Provider sign key" +msgstr "签名 Key" + +#: settings/serializers/auth/oidc.py:88 +msgid "Scopes" +msgstr "连接范围" + +#: settings/serializers/auth/oidc.py:90 +msgid "Id token max age" +msgstr "令牌有效时间" + +#: settings/serializers/auth/oidc.py:93 +msgid "Id token include claims" +msgstr "声明" + +#: settings/serializers/auth/oidc.py:95 +msgid "Use state" +msgstr "使用状态" + +#: settings/serializers/auth/oidc.py:96 +msgid "Use nonce" +msgstr "临时使用" + +#: settings/serializers/auth/radius.py:13 +msgid "Radius" +msgstr "Radius" + +#: settings/serializers/auth/radius.py:15 +msgid "Enable Radius Auth" +msgstr "启用 Radius 认证" + +#: settings/serializers/auth/radius.py:21 +msgid "OTP in Radius" +msgstr "使用 Radius OTP" + +#: settings/serializers/auth/saml2.py:11 +msgid "SAML2" +msgstr "SAML2" + +#: settings/serializers/auth/saml2.py:14 +msgid "Enable SAML2 Auth" +msgstr "启用 SAML2 认证" + +#: settings/serializers/auth/saml2.py:17 +msgid "IDP metadata URL" +msgstr "IDP metadata 地址" + +#: settings/serializers/auth/saml2.py:20 +msgid "IDP metadata XML" +msgstr "IDP metadata XML" + +#: settings/serializers/auth/saml2.py:23 +msgid "SP advanced settings" +msgstr "高级设置" + +#: settings/serializers/auth/saml2.py:27 +msgid "SP private key" +msgstr "SP 密钥" + +#: settings/serializers/auth/saml2.py:31 +msgid "SP cert" +msgstr "SP 证书" + +#: settings/serializers/auth/sms.py:15 +msgid "Enable SMS" +msgstr "启用 SMS" + +#: settings/serializers/auth/sms.py:17 +msgid "SMS provider / Protocol" +msgstr "短信服务商 / 协议" + +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +msgid "Signature" +msgstr "签名" + +#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:46 +#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 +msgid "Template code" +msgstr "模板" + +#: settings/serializers/auth/sms.py:31 +msgid "Test phone" +msgstr "测试手机号" + +#: settings/serializers/auth/sms.py:60 +msgid "App Access Address" +msgstr "应用地址" + +#: settings/serializers/auth/sms.py:61 +msgid "Signature channel number" +msgstr "签名通道号" + +#: settings/serializers/auth/sms.py:69 +msgid "Enterprise code(SP id)" +msgstr "企业代码(SP id)" + +#: settings/serializers/auth/sms.py:70 +msgid "Shared secret(Shared secret)" +msgstr "共享密码(Shared secret)" + +#: settings/serializers/auth/sms.py:71 +msgid "Original number(Src id)" +msgstr "原始号码(Src id)" + +#: settings/serializers/auth/sms.py:72 +msgid "Business type(Service id)" +msgstr "业务类型(Service id)" + +#: settings/serializers/auth/sms.py:75 +msgid "Template" +msgstr "模板" + +#: settings/serializers/auth/sms.py:76 +#, python-brace-format +msgid "" +"Template need contain {code} and Signature + template length does not exceed " +"67 words. For example, your verification code is {code}, which is valid for " +"5 minutes. Please do not disclose it to others." +msgstr "" +"模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 " +"{code}, 有效期为5分钟。请不要泄露给其他人。" + +#: settings/serializers/auth/sms.py:85 +#, python-brace-format +msgid "The template needs to contain {code}" +msgstr "模板需要包含 {code}" + +#: settings/serializers/auth/sms.py:88 +msgid "Signature + Template must not exceed 65 words" +msgstr "模板+签名不能超过65个字" + +#: settings/serializers/auth/sso.py:13 +msgid "Enable SSO auth" +msgstr "启用 SSO 令牌认证" + +#: settings/serializers/auth/sso.py:14 +msgid "Other service can using SSO token login to JumpServer without password" +msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过程" + +#: settings/serializers/auth/sso.py:17 +msgid "SSO auth key TTL" +msgstr "令牌有效期" + +#: settings/serializers/auth/sso.py:17 +#: xpack/plugins/cloud/serializers/account_attrs.py:176 +msgid "Unit: second" +msgstr "单位: 秒" + +#: settings/serializers/auth/wecom.py:15 +msgid "Enable WeCom Auth" +msgstr "启用企业微信认证" + +#: settings/serializers/basic.py:9 +msgid "Subject" +msgstr "主题" + +#: settings/serializers/basic.py:13 +msgid "More url" +msgstr "更多信息 URL" + +#: settings/serializers/basic.py:30 +msgid "Site url" +msgstr "当前站点URL" + +#: settings/serializers/basic.py:31 +msgid "eg: http://dev.jumpserver.org:8080" +msgstr "如: http://dev.jumpserver.org:8080" + +#: settings/serializers/basic.py:34 +msgid "User guide url" +msgstr "用户向导URL" + +#: settings/serializers/basic.py:35 +msgid "User first login update profile done redirect to it" +msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" + +#: settings/serializers/basic.py:39 +msgid "" +"The forgot password url on login page, If you use ldap or cas external " +"authentication, you can set it" +msgstr "" +"登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重" +"置密码访问的地址" + +#: settings/serializers/basic.py:43 +msgid "Global organization name" +msgstr "全局组织名" + +#: settings/serializers/basic.py:44 +msgid "The name of global organization to display" +msgstr "全局组织的显示名称,默认为 全局组织" + +#: settings/serializers/basic.py:46 +msgid "Enable announcement" +msgstr "启用公告" + +#: settings/serializers/basic.py:47 +msgid "Announcement" +msgstr "公告" + +#: settings/serializers/basic.py:48 +msgid "Enable tickets" +msgstr "启用工单" + +#: settings/serializers/cleaning.py:8 +msgid "Period clean" +msgstr "定時清掃" + +#: settings/serializers/cleaning.py:12 +msgid "Login log keep days" +msgstr "登录日志" + +#: settings/serializers/cleaning.py:12 settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:20 settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:28 +msgid "Unit: day" +msgstr "单位: 天" + +#: settings/serializers/cleaning.py:16 +msgid "Task log keep days" +msgstr "任务日志" + +#: settings/serializers/cleaning.py:20 +msgid "Operate log keep days" +msgstr "操作日志" + +#: settings/serializers/cleaning.py:24 +msgid "FTP log keep days" +msgstr "上传下载" + +#: settings/serializers/cleaning.py:28 +msgid "Cloud sync record keep days" +msgstr "云同步记录" + +#: settings/serializers/cleaning.py:31 +msgid "Session keep duration" +msgstr "会话日志保存时间" + +#: settings/serializers/cleaning.py:32 +msgid "" +"Unit: days, Session, record, command will be delete if more than duration, " +"only in database" +msgstr "" +"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" +"受影响)" + +#: settings/serializers/email.py:21 +msgid "SMTP host" +msgstr "SMTP 主机" + +#: settings/serializers/email.py:22 +msgid "SMTP port" +msgstr "SMTP 端口" + +#: settings/serializers/email.py:23 +msgid "SMTP account" +msgstr "SMTP 账号" + +#: settings/serializers/email.py:25 +msgid "SMTP password" +msgstr "SMTP 密码" + +#: settings/serializers/email.py:26 +msgid "Tips: Some provider use token except password" +msgstr "提示:一些邮件提供商需要输入的是授权码" + +#: settings/serializers/email.py:29 +msgid "Send user" +msgstr "发件人" + +#: settings/serializers/email.py:30 +msgid "Tips: Send mail account, default SMTP account as the send account" +msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" + +#: settings/serializers/email.py:33 +msgid "Test recipient" +msgstr "测试收件人" + +#: settings/serializers/email.py:34 +msgid "Tips: Used only as a test mail recipient" +msgstr "提示:仅用来作为测试邮件收件人" + +#: settings/serializers/email.py:38 +msgid "If SMTP port is 465, may be select" +msgstr "如果SMTP端口是465,通常需要启用 SSL" + +#: settings/serializers/email.py:41 +msgid "Use TLS" +msgstr "使用 TLS" + +#: settings/serializers/email.py:42 +msgid "If SMTP port is 587, may be select" +msgstr "如果SMTP端口是587,通常需要启用 TLS" + +#: settings/serializers/email.py:45 +msgid "Subject prefix" +msgstr "主题前缀" + +#: settings/serializers/email.py:54 +msgid "Create user email subject" +msgstr "邮件主题" + +#: settings/serializers/email.py:55 +msgid "" +"Tips: When creating a user, send the subject of the email (eg:Create account " +"successfully)" +msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" + +#: settings/serializers/email.py:59 +msgid "Create user honorific" +msgstr "邮件问候语" + +#: settings/serializers/email.py:60 +msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" +msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 你好)" + +#: settings/serializers/email.py:64 +msgid "Create user email content" +msgstr "邮件的内容" + +#: settings/serializers/email.py:65 +#, python-brace-format +msgid "" +"Tips: When creating a user, send the content of the email, support " +"{username} {name} {email} label" +msgstr "" +"提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" + +#: settings/serializers/email.py:69 +msgid "Tips: Email signature (eg:jumpserver)" +msgstr "邮件署名 (如:jumpserver)" + +#: settings/serializers/other.py:6 +msgid "More..." +msgstr "更多..." + +#: settings/serializers/other.py:9 +msgid "Email suffix" +msgstr "邮件后缀" + +#: settings/serializers/other.py:10 +msgid "" +"This is used by default if no email is returned during SSO authentication" +msgstr "SSO认证时,如果没有返回邮件地址,将使用该后缀" + +#: settings/serializers/other.py:14 +msgid "OTP issuer name" +msgstr "OTP 扫描后的名称" + +#: settings/serializers/other.py:18 +msgid "OTP valid window" +msgstr "OTP 延迟有效次数" + +#: settings/serializers/other.py:23 +msgid "CMD" +msgstr "CMD" + +#: settings/serializers/other.py:24 +msgid "PowerShell" +msgstr "PowerShell" + +#: settings/serializers/other.py:26 +msgid "Shell (Windows)" +msgstr "Windows shell" + +#: settings/serializers/other.py:27 +msgid "The shell type used when Windows assets perform ansible tasks" +msgstr "windows 资产执行 Ansible 任务时,使用的 Shell 类型。" + +#: settings/serializers/other.py:31 +msgid "Perm ungroup node" +msgstr "显示未分组节点" + +#: settings/serializers/other.py:32 +msgid "Perm single to ungroup node" +msgstr "" +"放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问" +"题" + +#: settings/serializers/other.py:37 +msgid "Ticket authorize default time" +msgstr "默认工单授权时间" + +#: settings/serializers/other.py:40 +msgid "day" +msgstr "天" + +#: settings/serializers/other.py:40 +msgid "hour" +msgstr "时" + +#: settings/serializers/other.py:41 +msgid "Ticket authorize default time unit" +msgstr "默认工单授权时间单位" + +#: settings/serializers/other.py:44 +msgid "Help Docs URL" +msgstr "文档链接" + +#: settings/serializers/other.py:45 +msgid "default: http://docs.jumpserver.org" +msgstr "默认: http://dev.jumpserver.org:8080" + +#: settings/serializers/other.py:49 +msgid "Help Support URL" +msgstr "支持链接" + +#: settings/serializers/other.py:50 +msgid "default: http://www.jumpserver.org/support/" +msgstr "默认: http://www.jumpserver.org/support/" + +#: settings/serializers/security.py:10 +msgid "Password minimum length" +msgstr "密码最小长度" + +#: settings/serializers/security.py:14 +msgid "Admin user password minimum length" +msgstr "管理员密码最小长度" + +#: settings/serializers/security.py:17 +msgid "Must contain capital" +msgstr "必须包含大写字符" + +#: settings/serializers/security.py:20 +msgid "Must contain lowercase" +msgstr "必须包含小写字符" + +#: settings/serializers/security.py:23 +msgid "Must contain numeric" +msgstr "必须包含数字" + +#: settings/serializers/security.py:26 +msgid "Must contain special" +msgstr "必须包含特殊字符" + +#: settings/serializers/security.py:31 +msgid "" +"Unit: minute, If the user has failed to log in for a limited number of " +"times, no login is allowed during this time interval." +msgstr "单位:分, 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" + +#: settings/serializers/security.py:40 +msgid "All users" +msgstr "所有用户" + +#: settings/serializers/security.py:41 +msgid "Only admin users" +msgstr "仅管理员" + +#: settings/serializers/security.py:43 +msgid "Global MFA auth" +msgstr "全局启用 MFA 认证" + +#: settings/serializers/security.py:47 +msgid "Third-party login users perform MFA authentication" +msgstr "第三方登录用户进行MFA认证" + +#: settings/serializers/security.py:48 +msgid "The third-party login modes include OIDC, CAS, and SAML2" +msgstr "第三方登录方式包括: OIDC、CAS、SAML2" + +#: settings/serializers/security.py:52 +msgid "Limit the number of user login failures" +msgstr "限制用户登录失败次数" + +#: settings/serializers/security.py:56 +msgid "Block user login interval" +msgstr "禁止用户登录时间间隔" + +#: settings/serializers/security.py:61 +msgid "Limit the number of IP login failures" +msgstr "限制 IP 登录失败次数" + +#: settings/serializers/security.py:65 +msgid "Block IP login interval" +msgstr "禁止 IP 登录时间间隔" + +#: settings/serializers/security.py:69 +msgid "Login IP White List" +msgstr "IP 登录白名单" + +#: settings/serializers/security.py:74 +msgid "Login IP Black List" +msgstr "IP 登录黑名单" + +#: settings/serializers/security.py:80 +msgid "User password expiration" +msgstr "用户密码过期时间" + +#: settings/serializers/security.py:82 +msgid "" +"Unit: day, If the user does not update the password during the time, the " +"user password will expire failure;The password expiration reminder mail will " +"be automatic sent to the user by system within 5 days (daily) before the " +"password expires" +msgstr "" +"单位:天, 如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件" +"将在密码过期前5天内由系统(每天)自动发送给用户" + +#: settings/serializers/security.py:89 +msgid "Number of repeated historical passwords" +msgstr "不能设置近几次密码" + +#: settings/serializers/security.py:91 +msgid "" +"Tip: When the user resets the password, it cannot be the previous n " +"historical passwords of the user" +msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" + +#: settings/serializers/security.py:96 +msgid "Only single device login" +msgstr "仅一台设备登录" + +#: settings/serializers/security.py:97 +msgid "Next device login, pre login will be logout" +msgstr "下个设备登录,上次登录会被顶掉" + +#: settings/serializers/security.py:100 +msgid "Only exist user login" +msgstr "仅已存在用户登录" + +#: settings/serializers/security.py:101 +msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet" +msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败" + +#: settings/serializers/security.py:104 +msgid "Only from source login" +msgstr "仅从用户来源登录" + +#: settings/serializers/security.py:105 +msgid "Only log in from the user source property" +msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败" + +#: settings/serializers/security.py:109 +msgid "MFA verify TTL" +msgstr "MFA 校验有效期" + +#: settings/serializers/security.py:111 +msgid "" +"Unit: second, The verification MFA takes effect only when you view the " +"account password" +msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效" + +#: settings/serializers/security.py:116 +msgid "Enable Login dynamic code" +msgstr "启用登录附加码" + +#: settings/serializers/security.py:117 +msgid "" +"The password and additional code are sent to a third party authentication " +"system for verification" +msgstr "" +"密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密" +"码+6位数字 完成认证" + +#: settings/serializers/security.py:122 +msgid "MFA in login page" +msgstr "MFA 在登录页面输入" + +#: settings/serializers/security.py:123 +msgid "Eu security regulations(GDPR) require MFA to be on the login page" +msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全" + +#: settings/serializers/security.py:126 +msgid "Enable Login captcha" +msgstr "启用登录验证码" + +#: settings/serializers/security.py:127 +msgid "Enable captcha to prevent robot authentication" +msgstr "开启验证码,防止机器人登录" + +#: settings/serializers/security.py:146 +msgid "Security" +msgstr "安全" + +#: settings/serializers/security.py:149 +msgid "Enable terminal register" +msgstr "终端注册" + +#: settings/serializers/security.py:151 +msgid "" +"Allow terminal register, after all terminal setup, you should disable this " +"for security" +msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" + +#: settings/serializers/security.py:155 +msgid "Enable watermark" +msgstr "开启水印" + +#: settings/serializers/security.py:156 +msgid "Enabled, the web session and replay contains watermark information" +msgstr "启用后,Web 会话和录像将包含水印信息" + +#: settings/serializers/security.py:160 +msgid "Connection max idle time" +msgstr "连接最大空闲时间" + +#: settings/serializers/security.py:161 +msgid "If idle time more than it, disconnect connection Unit: minute" +msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" + +#: settings/serializers/security.py:164 +msgid "Remember manual auth" +msgstr "保存手动输入密码" + +#: settings/serializers/security.py:167 +msgid "Enable change auth secure mode" +msgstr "启用改密安全模式" + +#: settings/serializers/security.py:170 +msgid "Insecure command alert" +msgstr "危险命令告警" + +#: settings/serializers/security.py:173 +msgid "Email recipient" +msgstr "邮件收件人" + +#: settings/serializers/security.py:174 +msgid "Multiple user using , split" +msgstr "多个用户,使用 , 分割" + +#: settings/serializers/security.py:177 +msgid "Batch command execution" +msgstr "批量命令执行" + +#: settings/serializers/security.py:178 +msgid "Allow user run batch command or not using ansible" +msgstr "是否允许用户使用 ansible 执行批量命令" + +#: settings/serializers/security.py:181 +msgid "Session share" +msgstr "会话分享" + +#: settings/serializers/security.py:182 +msgid "Enabled, Allows user active session to be shared with other users" +msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" + +#: settings/serializers/security.py:185 +msgid "Remote Login Protection" +msgstr "异地登录保护" + +#: settings/serializers/security.py:187 +msgid "" +"The system determines whether the login IP address belongs to a common login " +"city. If the account is logged in from a common login city, the system sends " +"a remote login reminder" +msgstr "" +"根据登录 IP 是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地" +"登录提醒" + +#: settings/serializers/terminal.py:15 +msgid "Auto" +msgstr "自动" + +#: settings/serializers/terminal.py:21 +msgid "Password auth" +msgstr "密码认证" + +#: settings/serializers/terminal.py:23 +msgid "Public key auth" +msgstr "密钥认证" + +#: settings/serializers/terminal.py:24 +msgid "" +"Tips: If use other auth method, like AD/LDAP, you should disable this to " +"avoid being able to log in after deleting" +msgstr "" +"提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" +"除后,还可以登录" + +#: settings/serializers/terminal.py:28 +msgid "List sort by" +msgstr "资产列表排序" + +#: settings/serializers/terminal.py:31 +msgid "List page size" +msgstr "资产列表每页数量" + +#: settings/serializers/terminal.py:34 +msgid "Telnet login regex" +msgstr "Telnet 成功正则表达式" + +#: settings/serializers/terminal.py:35 +msgid "" +"Tips: The login success message varies with devices. if you cannot log in to " +"the device through Telnet, set this parameter" +msgstr "" +"提示: 不同设备登录成功提示不一样,所以如果 telnet 不能正常登录,可以这里设置" + +#: settings/serializers/terminal.py:38 +msgid "Enable database proxy" +msgstr "启用数据库组件" + +#: settings/serializers/terminal.py:39 +msgid "Enable Razor" +msgstr "启用 Razor 服务" + +#: settings/serializers/terminal.py:40 +msgid "Enable SSH Client" +msgstr "启用 SSH Client" + +#: settings/serializers/terminal.py:51 +msgid "Default graphics resolution" +msgstr "默认图形化分辨率" + +#: settings/serializers/terminal.py:52 +msgid "" +"Tip: Default resolution to use when connecting graphical assets in Luna pages" +msgstr "提示:在Luna 页面中连接图形化资产时默认使用的分辨率" + +#: settings/utils/ldap.py:467 +msgid "ldap:// or ldaps:// protocol is used." +msgstr "使用 ldap:// 或 ldaps:// 协议" + +#: settings/utils/ldap.py:478 +msgid "Host or port is disconnected: {}" +msgstr "主机或端口不可连接: {}" + +#: settings/utils/ldap.py:480 +msgid "The port is not the port of the LDAP service: {}" +msgstr "端口不是LDAP服务端口: {}" + +#: settings/utils/ldap.py:482 +msgid "Please add certificate: {}" +msgstr "请添加证书" + +#: settings/utils/ldap.py:486 settings/utils/ldap.py:513 +#: settings/utils/ldap.py:543 settings/utils/ldap.py:571 +msgid "Unknown error: {}" +msgstr "未知错误: {}" + +#: settings/utils/ldap.py:500 +msgid "Bind DN or Password incorrect" +msgstr "绑定DN或密码错误" + +#: settings/utils/ldap.py:507 +msgid "Please enter Bind DN: {}" +msgstr "请输入绑定DN: {}" + +#: settings/utils/ldap.py:509 +msgid "Please enter Password: {}" +msgstr "请输入密码: {}" + +#: settings/utils/ldap.py:511 +msgid "Please enter correct Bind DN and Password: {}" +msgstr "请输入正确的绑定DN和密码: {}" + +#: settings/utils/ldap.py:529 +msgid "Invalid User OU or User search filter: {}" +msgstr "不合法的用户OU或用户过滤器: {}" + +#: settings/utils/ldap.py:560 +msgid "LDAP User attr map not include: {}" +msgstr "LDAP属性映射没有包含: {}" + +#: settings/utils/ldap.py:567 +msgid "LDAP User attr map is not dict" +msgstr "LDAP属性映射不合法" + +#: settings/utils/ldap.py:586 +msgid "LDAP authentication is not enabled" +msgstr "LDAP认证没有启用" + +#: settings/utils/ldap.py:604 +msgid "Error (Invalid LDAP server): {}" +msgstr "错误 (不合法的LDAP服务器地址): {}" + +#: settings/utils/ldap.py:606 +msgid "Error (Invalid Bind DN): {}" +msgstr "错误(不合法的绑定DN): {}" + +#: settings/utils/ldap.py:608 +msgid "Error (Invalid LDAP User attr map): {}" +msgstr "错误(不合法的LDAP属性映射): {}" + +#: settings/utils/ldap.py:610 +msgid "Error (Invalid User OU or User search filter): {}" +msgstr "错误(不合法的用户OU或用户过滤器): {}" + +#: settings/utils/ldap.py:612 +msgid "Error (Not enabled LDAP authentication): {}" +msgstr "错误(没有启用LDAP认证): {}" + +#: settings/utils/ldap.py:614 +msgid "Error (Unknown): {}" +msgstr "错误(未知): {}" + +#: settings/utils/ldap.py:617 +msgid "Succeed: Match {} s user" +msgstr "成功匹配 {} 个用户" + +#: settings/utils/ldap.py:650 +msgid "Authentication failed (configuration incorrect): {}" +msgstr "认证失败(配置错误): {}" + +#: settings/utils/ldap.py:654 +msgid "Authentication failed (username or password incorrect): {}" +msgstr "认证失败 (用户名或密码不正确): {}" + +#: settings/utils/ldap.py:656 +msgid "Authentication failed (Unknown): {}" +msgstr "认证失败: (未知): {}" + +#: settings/utils/ldap.py:659 +msgid "Authentication success: {}" +msgstr "认证成功: {}" + +#: templates/_csv_import_export.html:8 +msgid "Export" +msgstr "导出" + +#: templates/_csv_import_export.html:13 templates/_csv_import_modal.html:5 +msgid "Import" +msgstr "导入" + +#: templates/_csv_import_modal.html:12 +msgid "Download the imported template or use the exported CSV file format" +msgstr "下载导入的模板或使用导出的csv格式" + +#: templates/_csv_import_modal.html:13 +msgid "Download the import template" +msgstr "下载导入模版" + +#: templates/_csv_import_modal.html:17 templates/_csv_update_modal.html:17 +msgid "Select the CSV file to import" +msgstr "请选择csv文件导入" + +#: templates/_csv_import_modal.html:39 templates/_csv_update_modal.html:42 +msgid "Please select file" +msgstr "选择文件" + +#: templates/_csv_update_modal.html:12 +msgid "Download the update template or use the exported CSV file format" +msgstr "下载更新的模板或使用导出的csv格式" + +#: templates/_csv_update_modal.html:13 +msgid "Download the update template" +msgstr "下载更新模版" + +#: templates/_header_bar.html:12 +msgid "Help" +msgstr "帮助" + +#: templates/_header_bar.html:19 +msgid "Docs" +msgstr "文档" + +#: templates/_header_bar.html:25 +msgid "Commercial support" +msgstr "商业支持" + +#: templates/_header_bar.html:76 users/forms/profile.py:44 +msgid "Profile" +msgstr "个人信息" + +#: templates/_header_bar.html:79 +msgid "Admin page" +msgstr "管理页面" + +#: templates/_header_bar.html:81 +msgid "User page" +msgstr "用户页面" + +#: templates/_header_bar.html:84 +msgid "API Key" +msgstr "API Key" + +#: templates/_header_bar.html:85 +msgid "Logout" +msgstr "注销登录" + +#: templates/_message.html:6 +msgid "" +"\n" +" Your account has expired, please contact the administrator.\n" +" " +msgstr "" +"\n" +" 您的账号已经过期,请联系管理员。 " + +#: templates/_message.html:13 +msgid "Your account will at" +msgstr "您的账号将于" + +#: templates/_message.html:13 templates/_message.html:30 +msgid "expired. " +msgstr "过期。" + +#: templates/_message.html:23 +#, python-format +msgid "" +"\n" +" Your password has expired, please click this link update password.\n" +" " +msgstr "" +"\n" +" 您的密码已经过期,请点击 链接 更新密码\n" +" " + +#: templates/_message.html:30 +msgid "Your password will at" +msgstr "您的密码将于" + +#: templates/_message.html:31 +#, python-format +msgid "" +"\n" +" please click this " +"link to update your password.\n" +" " +msgstr "" +"\n" +" 请点击 链接 更" +"新密码\n" +" " + +#: templates/_message.html:43 +#, python-format +msgid "" +"\n" +" Your information was incomplete. Please click this link to complete your information.\n" +" " +msgstr "" +"\n" +" 你的信息不完整,请点击 链接 " +" 补充完整\n" +" " + +#: templates/_message.html:56 +#, python-format +msgid "" +"\n" +" Your ssh public key not set or expired. Please click this link to update\n" +" " +msgstr "" +"\n" +" 您的SSH密钥没有设置或已失效,请点击 链接 更新\n" +" " + +#: templates/_mfa_login_field.html:28 +msgid "Send verification code" +msgstr "发送验证码" + +#: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:129 +msgid "Wait: " +msgstr "等待:" + +#: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:145 +msgid "The verification code has been sent" +msgstr "验证码已发送" + +#: templates/_without_nav_base.html:26 +msgid "Home page" +msgstr "首页" + +#: templates/resource_download.html:18 templates/resource_download.html:31 +msgid "Client" +msgstr "客户端" + +#: templates/resource_download.html:20 +msgid "" +"JumpServer Client, currently used to launch the client, now only support " +"launch RDP SSH client, The Telnet client will next" +msgstr "" +"JumpServer 客户端,目前用来唤起 特定客户端程序 连接资产, 目前仅支持 RDP SSH " +"客户端,Telnet 会在未来支持" + +#: templates/resource_download.html:31 +msgid "Microsoft" +msgstr "微软" + +#: templates/resource_download.html:31 +msgid "Official" +msgstr "官方" + +#: templates/resource_download.html:33 +msgid "" +"macOS needs to download the client to connect RDP asset, which comes with " +"Windows" +msgstr "macOS 需要下载客户端来连接 RDP 资产,Windows 系统默认安装了该程序" + +#: templates/resource_download.html:42 +msgid "Windows Remote application publisher tools" +msgstr "Windows 远程应用发布服务器工具" + +#: templates/resource_download.html:43 +msgid "" +"OpenSSH is a program used to connect remote applications in the Windows " +"Remote Application Publisher" +msgstr "OpenSSH 是在 windows 远程应用发布服务器中用来连接远程应用的程序" + +#: templates/resource_download.html:48 +msgid "" +"Jmservisor is the program used to pull up remote applications in Windows " +"Remote Application publisher" +msgstr "Jmservisor 是在 windows 远程应用发布服务器中用来拉起远程应用的程序" + +#: templates/resource_download.html:57 +msgid "Offline video player" +msgstr "离线录像播放器" + +#: terminal/api/component/endpoint.py:31 +msgid "Not found protocol query params" +msgstr "" + +#: terminal/api/component/storage.py:28 +msgid "Deleting the default storage is not allowed" +msgstr "不允许删除默认存储配置" + +#: terminal/api/component/storage.py:31 +msgid "Cannot delete storage that is being used" +msgstr "不允许删除正在使用的存储配置" + +#: terminal/api/component/storage.py:72 terminal/api/component/storage.py:73 +msgid "Command storages" +msgstr "命令存储" + +#: terminal/api/component/storage.py:79 +msgid "Invalid" +msgstr "无效" + +#: terminal/api/component/storage.py:119 +msgid "Test failure: {}" +msgstr "测试失败: {}" + +#: terminal/api/component/storage.py:122 +msgid "Test successful" +msgstr "测试成功" + +#: terminal/api/component/storage.py:124 +msgid "Test failure: Account invalid" +msgstr "测试失败: 账号无效" + +#: terminal/api/component/terminal.py:35 +msgid "Have online sessions" +msgstr "有在线会话" + +#: terminal/api/session/session.py:217 +msgid "Session does not exist: {}" +msgstr "会话不存在: {}" + +#: terminal/api/session/session.py:220 +msgid "Session is finished or the protocol not supported" +msgstr "会话已经完成或协议不支持" + +#: terminal/api/session/session.py:233 +msgid "User does not have permission" +msgstr "用户没有权限" + +#: terminal/api/session/sharing.py:29 +msgid "Secure session sharing settings is disabled" +msgstr "未开启会话共享" + +#: terminal/apps.py:9 +msgid "Terminals" +msgstr "终端管理" + +#: terminal/backends/command/models.py:16 +msgid "Ordinary" +msgstr "普通" + +#: terminal/backends/command/models.py:17 +msgid "Dangerous" +msgstr "危险" + +#: terminal/backends/command/models.py:23 +msgid "Input" +msgstr "输入" + +#: terminal/backends/command/models.py:24 +#: terminal/backends/command/serializers.py:38 +msgid "Output" +msgstr "输出" + +#: terminal/backends/command/models.py:25 terminal/models/session/replay.py:9 +#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77 +#: terminal/templates/terminal/_msg_command_alert.html:10 +#: tickets/models/ticket/command_confirm.py:15 +msgid "Session" +msgstr "会话" + +#: terminal/backends/command/models.py:26 +#: terminal/backends/command/serializers.py:18 +msgid "Risk level" +msgstr "风险等级" + +#: terminal/backends/command/serializers.py:16 +msgid "Session ID" +msgstr "会话ID" + +#: terminal/backends/command/serializers.py:37 +msgid "Account " +msgstr "账号" + +#: terminal/backends/command/serializers.py:39 +msgid "Timestamp" +msgstr "时间戳" + +#: terminal/backends/command/serializers.py:41 +#: terminal/models/component/terminal.py:84 +msgid "Remote Address" +msgstr "远端地址" + +#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 +#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 +#: terminal/connect_methods.py:50 +msgid "DB Client" +msgstr "数据库客户端" + +#: terminal/const.py:30 +msgid "Critical" +msgstr "严重" + +#: terminal/const.py:31 +msgid "High" +msgstr "较高" + +#: terminal/const.py:32 terminal/serializers/session.py:17 +#: users/templates/users/reset_password.html:50 +msgid "Normal" +msgstr "正常" + +#: terminal/const.py:33 +msgid "Offline" +msgstr "离线" + +#: terminal/exceptions.py:8 +msgid "Bulk create not support" +msgstr "不支持批量创建" + +#: terminal/exceptions.py:13 +msgid "Storage is invalid" +msgstr "存储无效" + +#: terminal/models/applet/applet.py:25 +msgid "Author" +msgstr "作者" + +#: terminal/models/applet/applet.py:30 +msgid "Tags" +msgstr "标签" + +#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +msgid "Hosts" +msgstr "主机" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +msgid "Deploy options" +msgstr "部署参数" + +#: terminal/models/applet/host.py:19 +msgid "Inited" +msgstr "已初始化" + +#: terminal/models/applet/host.py:20 +msgid "Date inited" +msgstr "初始化日期" + +#: terminal/models/applet/host.py:21 +msgid "Date synced" +msgstr "同步日期" + +#: terminal/models/applet/host.py:33 +msgid "Applet host" +msgstr "远程应用发布机" + +#: terminal/models/applet/host.py:105 +msgid "Hosting" +msgstr "宿主机" + +#: terminal/models/applet/host.py:106 +msgid "Initial" +msgstr "初始化" + +#: terminal/models/component/endpoint.py:15 +msgid "HTTPS Port" +msgstr "HTTPS 端口" + +#: terminal/models/component/endpoint.py:16 +msgid "HTTP Port" +msgstr "HTTP 端口" + +#: terminal/models/component/endpoint.py:17 +msgid "SSH Port" +msgstr "SSH 端口" + +#: terminal/models/component/endpoint.py:18 +msgid "RDP Port" +msgstr "RDP 端口" + +#: terminal/models/component/endpoint.py:25 +#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 +#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 +#: terminal/serializers/storage.py:98 +msgid "Endpoint" +msgstr "端点" + +#: terminal/models/component/endpoint.py:87 +msgid "IP group" +msgstr "IP 组" + +#: terminal/models/component/endpoint.py:99 +msgid "Endpoint rule" +msgstr "端点规则" + +#: terminal/models/component/status.py:14 +msgid "Session Online" +msgstr "在线会话" + +#: terminal/models/component/status.py:15 +msgid "CPU Load" +msgstr "CPU负载" + +#: terminal/models/component/status.py:16 +msgid "Memory Used" +msgstr "内存使用" + +#: terminal/models/component/status.py:17 +msgid "Disk Used" +msgstr "磁盘使用" + +#: terminal/models/component/status.py:18 +msgid "Connections" +msgstr "连接数" + +#: terminal/models/component/status.py:19 +msgid "Threads" +msgstr "线程数" + +#: terminal/models/component/status.py:20 +msgid "Boot Time" +msgstr "运行时间" + +#: terminal/models/component/storage.py:28 +msgid "Default storage" +msgstr "默认存储" + +#: terminal/models/component/storage.py:140 +#: terminal/models/component/terminal.py:85 +msgid "Command storage" +msgstr "命令存储" + +#: terminal/models/component/storage.py:200 +#: terminal/models/component/terminal.py:86 +msgid "Replay storage" +msgstr "录像存储" + +#: terminal/models/component/terminal.py:82 +msgid "type" +msgstr "类型" + +#: terminal/models/component/terminal.py:87 +msgid "Application User" +msgstr "应用用户" + +#: terminal/models/component/terminal.py:158 +msgid "Can view terminal config" +msgstr "可以查看终端配置" + +#: terminal/models/session/command.py:66 +msgid "Command record" +msgstr "命令记录" + +#: terminal/models/session/replay.py:12 +msgid "Session replay" +msgstr "会话录像" + +#: terminal/models/session/replay.py:14 +msgid "Can upload session replay" +msgstr "可以上传会话录像" + +#: terminal/models/session/replay.py:15 +msgid "Can download session replay" +msgstr "可以下载会话录像" + +#: terminal/models/session/session.py:35 terminal/models/session/sharing.py:100 +msgid "Login from" +msgstr "登录来源" + +#: terminal/models/session/session.py:40 +msgid "Replay" +msgstr "回放" + +#: terminal/models/session/session.py:44 +msgid "Date end" +msgstr "结束日期" + +#: terminal/models/session/session.py:237 +msgid "Session record" +msgstr "会话记录" + +#: terminal/models/session/session.py:239 +msgid "Can monitor session" +msgstr "可以监控会话" + +#: terminal/models/session/session.py:240 +msgid "Can share session" +msgstr "可以分享会话" + +#: terminal/models/session/session.py:241 +msgid "Can terminate session" +msgstr "可以终断会话" + +#: terminal/models/session/session.py:242 +msgid "Can validate session action perm" +msgstr "可以验证会话动作权限" + +#: terminal/models/session/sharing.py:30 +msgid "Expired time (min)" +msgstr "过期时间 (分)" + +#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82 +msgid "Session sharing" +msgstr "会话分享" + +#: terminal/models/session/sharing.py:38 +msgid "Can add super session sharing" +msgstr "可以创建超级会话分享" + +#: terminal/models/session/sharing.py:65 +msgid "Link not active" +msgstr "链接失效" + +#: terminal/models/session/sharing.py:67 +msgid "Link expired" +msgstr "链接过期" + +#: terminal/models/session/sharing.py:69 +msgid "User not allowed to join" +msgstr "该用户无权加入会话" + +#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59 +msgid "Joiner" +msgstr "加入者" + +#: terminal/models/session/sharing.py:89 +msgid "Date joined" +msgstr "加入日期" + +#: terminal/models/session/sharing.py:92 +msgid "Date left" +msgstr "结束日期" + +#: terminal/models/session/sharing.py:115 +msgid "Session join record" +msgstr "会话加入记录" + +#: terminal/models/session/sharing.py:131 +msgid "Invalid verification code" +msgstr "验证码不正确" + +#: terminal/notifications.py:22 +msgid "Sessions" +msgstr "会话管理" + +#: terminal/notifications.py:68 +msgid "Danger command alert" +msgstr "危险命令告警" + +#: terminal/notifications.py:95 terminal/notifications.py:143 +msgid "Level" +msgstr "级别" + +#: terminal/notifications.py:113 +msgid "Batch danger command alert" +msgstr "批量危险命令告警" + +#: terminal/serializers/applet.py:16 +msgid "Published" +msgstr "已发布" + +#: terminal/serializers/applet.py:17 +msgid "Unpublished" +msgstr "未发布" + +#: terminal/serializers/applet.py:18 +msgid "Not match" +msgstr "没有匹配的" + +#: terminal/serializers/applet.py:32 +msgid "Icon" +msgstr "图标" + +#: terminal/serializers/applet_host.py:21 +msgid "Per Session" +msgstr "每会话" + +#: terminal/serializers/applet_host.py:22 +msgid "Per Device" +msgstr "每设备" + +#: terminal/serializers/applet_host.py:28 +msgid "RDS Licensing" +msgstr "RDS 许可证" + +#: terminal/serializers/applet_host.py:29 +msgid "RDS License Server" +msgstr "RDS 许可服务器" + +#: terminal/serializers/applet_host.py:30 +msgid "RDS Licensing Mode" +msgstr "RDS 授权模式" + +#: terminal/serializers/applet_host.py:32 +msgid "RDS fSingleSessionPerUser" +msgstr "" + +#: terminal/serializers/applet_host.py:33 +msgid "RDS Max Disconnection Time" +msgstr "" + +#: terminal/serializers/applet_host.py:34 +msgid "RDS Remote App Logoff Time Limit" +msgstr "" + +#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +msgid "Load status" +msgstr "负载状态" + +#: terminal/serializers/endpoint.py:14 +msgid "Magnus listen db port" +msgstr "Magnus 监听的数据库端口" + +#: terminal/serializers/endpoint.py:17 +msgid "Magnus Listen port range" +msgstr "Magnus 监听的端口范围" + +#: terminal/serializers/endpoint.py:19 +msgid "" +"The range of ports that Magnus listens on is modified in the configuration " +"file" +msgstr "请在配置文件中修改 Magnus 监听的端口范围" + +#: terminal/serializers/endpoint.py:51 +msgid "" +"If asset IP addresses under different endpoints conflict, use asset labels" +msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" + +#: terminal/serializers/session.py:18 +msgid "Tunnel" +msgstr "" + +#: terminal/serializers/session.py:41 +msgid "User ID" +msgstr "用户 ID" + +#: terminal/serializers/session.py:42 +msgid "Asset ID" +msgstr "资产 ID" + +#: terminal/serializers/session.py:43 +msgid "Login from display" +msgstr "登录来源名称" + +#: terminal/serializers/session.py:45 +msgid "Can replay" +msgstr "是否可重放" + +#: terminal/serializers/session.py:46 +msgid "Can join" +msgstr "是否可加入" + +#: terminal/serializers/session.py:47 +msgid "Terminal ID" +msgstr "终端 ID" + +#: terminal/serializers/session.py:48 +msgid "Is finished" +msgstr "是否完成" + +#: terminal/serializers/session.py:49 +msgid "Can terminate" +msgstr "是否可中断" + +#: terminal/serializers/session.py:50 +msgid "Terminal display" +msgstr "终端显示" + +#: terminal/serializers/session.py:55 +msgid "Command amount" +msgstr "命令数量" + +#: terminal/serializers/storage.py:20 +msgid "Endpoint invalid: remove path `{}`" +msgstr "端点无效: 移除路径 `{}`" + +#: terminal/serializers/storage.py:26 +msgid "Bucket" +msgstr "桶名称" + +#: terminal/serializers/storage.py:30 +#: xpack/plugins/cloud/serializers/account_attrs.py:17 +msgid "Access key id" +msgstr "访问密钥 ID(AK)" + +#: terminal/serializers/storage.py:34 +#: xpack/plugins/cloud/serializers/account_attrs.py:20 +msgid "Access key secret" +msgstr "访问密钥密文(SK)" + +#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:217 +msgid "Region" +msgstr "地域" + +#: terminal/serializers/storage.py:109 +msgid "Container name" +msgstr "容器名称" + +#: terminal/serializers/storage.py:112 +msgid "Account key" +msgstr "账号密钥" + +#: terminal/serializers/storage.py:115 +msgid "Endpoint suffix" +msgstr "端点后缀" + +#: terminal/serializers/storage.py:135 +msgid "The address format is incorrect" +msgstr "地址格式不正确" + +#: terminal/serializers/storage.py:142 +msgid "Host invalid" +msgstr "主机无效" + +#: terminal/serializers/storage.py:145 +msgid "Port invalid" +msgstr "端口无效" + +#: terminal/serializers/storage.py:160 +msgid "Index by date" +msgstr "按日期建索引" + +#: terminal/serializers/storage.py:161 +msgid "Whether to create an index by date" +msgstr "是否根据日期动态建立索引" + +#: terminal/serializers/storage.py:164 +msgid "Index" +msgstr "索引" + +#: terminal/serializers/storage.py:166 +msgid "Doc type" +msgstr "文档类型" + +#: terminal/serializers/storage.py:168 +msgid "Ignore Certificate Verification" +msgstr "忽略证书认证" + +#: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 +msgid "Not found" +msgstr "没有发现" + +#: terminal/templates/terminal/_msg_command_alert.html:10 +msgid "view" +msgstr "查看" + +#: terminal/utils/db_port_mapper.py:77 +msgid "" +"No available port is matched. The number of databases may have exceeded the " +"number of ports open to the database agent service, Contact the " +"administrator to open more ports." +msgstr "" +"未匹配到可用端口,数据库的数量可能已经超过数据库代理服务开放的端口数量,请联" +"系管理员开放更多端口。" + +#: terminal/utils/db_port_mapper.py:103 +msgid "" +"No ports can be used, check and modify the limit on the number of ports that " +"Magnus listens on in the configuration file." +msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听的端口数量限制。" + +#: terminal/utils/db_port_mapper.py:105 +msgid "All available port count: {}, Already use port count: {}" +msgstr "所有可用端口数量:{},已使用端口数量:{}" + +#: tickets/apps.py:7 +msgid "Tickets" +msgstr "工单管理" + +#: tickets/const.py:9 +msgid "Apply for asset" +msgstr "申请资产" + +#: tickets/const.py:16 tickets/const.py:24 tickets/const.py:43 +msgid "Open" +msgstr "打开" + +#: tickets/const.py:18 tickets/const.py:31 +msgid "Reopen" +msgstr "" + +#: tickets/const.py:19 tickets/const.py:32 +msgid "Approved" +msgstr "已同意" + +#: tickets/const.py:20 tickets/const.py:33 +msgid "Rejected" +msgstr "已拒绝" + +#: tickets/const.py:30 tickets/const.py:38 +msgid "Closed" +msgstr "关闭的" + +#: tickets/const.py:46 +msgid "Approve" +msgstr "同意" + +#: tickets/const.py:50 +msgid "One level" +msgstr "1 级" + +#: tickets/const.py:51 +msgid "Two level" +msgstr "2 级" + +#: tickets/const.py:55 +msgid "Org admin" +msgstr "组织管理员" + +#: tickets/const.py:56 +msgid "Custom user" +msgstr "自定义用户" + +#: tickets/const.py:57 +msgid "Super admin" +msgstr "超级管理员" + +#: tickets/const.py:58 +msgid "Super admin and org admin" +msgstr "组织管理员或超级管理员" + +#: tickets/errors.py:9 +msgid "Ticket already closed" +msgstr "工单已经关闭" + +#: tickets/handlers/apply_asset.py:36 +msgid "" +"Created by the ticket ticket title: {} ticket applicant: {} ticket " +"processor: {} ticket ID: {}" +msgstr "" +"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" + +#: tickets/handlers/base.py:86 +msgid "Change field" +msgstr "变更字段" + +#: tickets/handlers/base.py:86 +msgid "Before change" +msgstr "变更前" + +#: tickets/handlers/base.py:86 +msgid "After change" +msgstr "变更后" + +#: tickets/handlers/base.py:98 +msgid "{} {} the ticket" +msgstr "{} {} 工单" + +#: tickets/models/comment.py:14 +msgid "common" +msgstr "" + +#: tickets/models/comment.py:23 +msgid "User display name" +msgstr "用户显示名称" + +#: tickets/models/comment.py:24 +msgid "Body" +msgstr "内容" + +#: tickets/models/flow.py:19 tickets/models/flow.py:61 +#: tickets/models/ticket/general.py:40 +msgid "Approve level" +msgstr "审批级别" + +#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +msgid "Approve strategy" +msgstr "审批策略" + +#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +msgid "Assignees" +msgstr "受理人" + +#: tickets/models/flow.py:33 +msgid "Ticket flow approval rule" +msgstr "工单批准信息" + +#: tickets/models/flow.py:66 +msgid "Ticket flow" +msgstr "工单流程" + +#: tickets/models/relation.py:10 +msgid "Ticket session relation" +msgstr "工单会话" + +#: tickets/models/ticket/apply_application.py:10 +#: tickets/models/ticket/apply_asset.py:13 +msgid "Permission name" +msgstr "授权规则名称" + +#: tickets/models/ticket/apply_application.py:19 +msgid "Apply applications" +msgstr "申请应用" + +#: tickets/models/ticket/apply_application.py:22 +msgid "Apply system users" +msgstr "申请的系统用户" + +#: tickets/models/ticket/apply_asset.py:9 +#: tickets/serializers/ticket/apply_asset.py:14 +msgid "Select at least one asset or node" +msgstr "资产或者节点至少选择一项" + +#: tickets/models/ticket/apply_asset.py:14 +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "申请节点" + +#: tickets/models/ticket/apply_asset.py:16 +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申请资产" + +#: tickets/models/ticket/apply_asset.py:17 +msgid "Apply accounts" +msgstr "申请账号" + +#: tickets/models/ticket/command_confirm.py:9 +msgid "Run user" +msgstr "运行的用户" + +#: tickets/models/ticket/command_confirm.py:11 +msgid "Run asset" +msgstr "运行的资产" + +#: tickets/models/ticket/command_confirm.py:12 +msgid "Run command" +msgstr "运行的命令" + +#: tickets/models/ticket/command_confirm.py:19 +msgid "Command filter acl" +msgstr "命令过滤器" + +#: tickets/models/ticket/general.py:75 +msgid "Ticket step" +msgstr "工单步骤" + +#: tickets/models/ticket/general.py:93 +msgid "Ticket assignee" +msgstr "工单受理人" + +#: tickets/models/ticket/general.py:272 +msgid "Title" +msgstr "标题" + +#: tickets/models/ticket/general.py:288 +msgid "Applicant" +msgstr "申请人" + +#: tickets/models/ticket/general.py:292 +msgid "TicketFlow" +msgstr "工单流程" + +#: tickets/models/ticket/general.py:295 +msgid "Approval step" +msgstr "审批步骤" + +#: tickets/models/ticket/general.py:298 +msgid "Relation snapshot" +msgstr "工单快照" + +#: tickets/models/ticket/general.py:392 +msgid "Please try again" +msgstr "请再次尝试" + +#: tickets/models/ticket/general.py:425 +msgid "Super ticket" +msgstr "超级工单" + +#: tickets/models/ticket/login_asset_confirm.py:11 +msgid "Login user" +msgstr "登录用户" + +#: tickets/models/ticket/login_asset_confirm.py:14 +msgid "Login asset" +msgstr "登录资产" + +#: tickets/models/ticket/login_asset_confirm.py:17 +msgid "Login account" +msgstr "登录账号" + +#: tickets/models/ticket/login_confirm.py:12 +msgid "Login datetime" +msgstr "登录日期" + +#: tickets/notifications.py:63 +msgid "Ticket basic info" +msgstr "工单基本信息" + +#: tickets/notifications.py:64 +msgid "Ticket applied info" +msgstr "工单申请信息" + +#: tickets/notifications.py:109 +msgid "Your has a new ticket, applicant - {}" +msgstr "你有一个新的工单, 申请人 - {}" + +#: tickets/notifications.py:113 +msgid "{}: New Ticket - {} ({})" +msgstr "新工单 - {} ({})" + +#: tickets/notifications.py:157 +msgid "Your ticket has been processed, processor - {}" +msgstr "你的工单已被处理, 处理人 - {}" + +#: tickets/notifications.py:161 +msgid "Ticket has processed - {} ({})" +msgstr "你的工单已被处理, 处理人 - {} ({})" + +#: tickets/serializers/flow.py:21 +msgid "Assignees display" +msgstr "受理人名称" + +#: tickets/serializers/flow.py:47 +msgid "Please select the Assignees" +msgstr "请选择受理人" + +#: tickets/serializers/flow.py:75 +msgid "The current organization type already exists" +msgstr "当前组织已存在该类型" + +#: tickets/serializers/super_ticket.py:11 +msgid "Processor" +msgstr "处理人" + +#: tickets/serializers/ticket/apply_asset.py:16 +msgid "Support fuzzy search, and display up to 10 items" +msgstr "支持模糊搜索,最多显示10项" + +#: tickets/serializers/ticket/apply_asset.py:28 +msgid "Apply actions" +msgstr "申请动作" + +#: tickets/serializers/ticket/common.py:15 +#: tickets/serializers/ticket/common.py:77 +msgid "Created by ticket ({}-{})" +msgstr "通过工单创建 ({}-{})" + +#: tickets/serializers/ticket/common.py:67 +msgid "The expiration date should be greater than the start date" +msgstr "过期时间要大于开始时间" + +#: tickets/serializers/ticket/common.py:84 +msgid "Permission named `{}` already exists" +msgstr "授权名称 `{}` 已存在" + +#: tickets/serializers/ticket/ticket.py:95 +msgid "The ticket flow `{}` does not exist" +msgstr "工单流程 `{}` 不存在" + +#: tickets/templates/tickets/_msg_ticket.html:20 +msgid "View details" +msgstr "查看详情" + +#: tickets/templates/tickets/_msg_ticket.html:25 +msgid "Direct approval" +msgstr "直接批准" + +#: tickets/templates/tickets/approve_check_password.html:11 +msgid "Ticket information" +msgstr "工单信息" + +#: tickets/templates/tickets/approve_check_password.html:29 +#: tickets/views/approve.py:38 +msgid "Ticket approval" +msgstr "工单审批" + +#: tickets/templates/tickets/approve_check_password.html:45 +msgid "Approval" +msgstr "同意" + +#: tickets/templates/tickets/approve_check_password.html:54 +msgid "Go Login" +msgstr "去登录" + +#: tickets/views/approve.py:39 +msgid "" +"This ticket does not exist, the process has ended, or this link has expired" +msgstr "工单不存在,或者工单流程已经结束,或者此链接已经过期" + +#: tickets/views/approve.py:68 +msgid "Click the button below to approve or reject" +msgstr "点击下方按钮同意或者拒绝" + +#: tickets/views/approve.py:70 +msgid "After successful authentication, this ticket can be approved directly" +msgstr "认证成功后,工单可直接审批" + +#: tickets/views/approve.py:92 +msgid "Illegal approval action" +msgstr "无效的审批动作" + +#: tickets/views/approve.py:105 +msgid "This user is not authorized to approve this ticket" +msgstr "此用户无权审批此工单" + +#: users/api/user.py:183 +msgid "Could not reset self otp, use profile reset instead" +msgstr "不能在该页面重置 MFA 多因子认证, 请去个人信息页面重置" + +#: users/apps.py:9 +msgid "Users" +msgstr "用户管理" + +#: users/const.py:10 +msgid "System administrator" +msgstr "系统管理员" + +#: users/const.py:11 +msgid "System auditor" +msgstr "系统审计员" + +#: users/const.py:12 +msgid "Organization administrator" +msgstr "组织管理员" + +#: users/const.py:13 +msgid "Organization auditor" +msgstr "组织审计员" + +#: users/const.py:18 +msgid "Reset link will be generated and sent to the user" +msgstr "生成重置密码链接,通过邮件发送给用户" + +#: users/const.py:19 +msgid "Set password" +msgstr "设置密码" + +#: users/exceptions.py:10 +msgid "MFA not enabled" +msgstr "MFA 多因子认证没有开启" + +#: users/exceptions.py:20 +msgid "MFA method not support" +msgstr "不支持该 MFA 方式" + +#: users/forms/profile.py:50 +msgid "" +"When enabled, you will enter the MFA binding process the next time you log " +"in. you can also directly bind in \"personal information -> quick " +"modification -> change MFA Settings\"!" +msgstr "" +"启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速" +"修改->设置 MFA 多因子认证)中直接绑定!" + +#: users/forms/profile.py:61 +msgid "* Enable MFA to make the account more secure." +msgstr "* 启用 MFA 多因子认证,使账号更加安全。" + +#: users/forms/profile.py:70 +msgid "" +"In order to protect you and your company, please keep your account, password " +"and key sensitive information properly. (for example: setting complex " +"password, enabling MFA)" +msgstr "" +"为了保护您和公司的安全,请妥善保管您的账号、密码和密钥等重要敏感信息;(如:" +"设置复杂密码,并启用 MFA 多因子认证)" + +#: users/forms/profile.py:77 +msgid "Finish" +msgstr "完成" + +#: users/forms/profile.py:84 +msgid "New password" +msgstr "新密码" + +#: users/forms/profile.py:89 +msgid "Confirm password" +msgstr "确认密码" + +#: users/forms/profile.py:97 +msgid "Password does not match" +msgstr "密码不一致" + +#: users/forms/profile.py:118 +msgid "Old password" +msgstr "原来密码" + +#: users/forms/profile.py:128 +msgid "Old password error" +msgstr "原来密码错误" + +#: users/forms/profile.py:138 +msgid "Automatically configure and download the SSH key" +msgstr "自动配置并下载SSH密钥" + +#: users/forms/profile.py:140 +msgid "ssh public key" +msgstr "SSH公钥" + +#: users/forms/profile.py:141 +msgid "ssh-rsa AAAA..." +msgstr "ssh-rsa AAAA..." + +#: users/forms/profile.py:142 +msgid "Paste your id_rsa.pub here." +msgstr "复制你的公钥到这里" + +#: users/forms/profile.py:155 +msgid "Public key should not be the same as your old one." +msgstr "不能和原来的密钥相同" + +#: users/forms/profile.py:159 users/serializers/profile.py:100 +#: users/serializers/profile.py:183 users/serializers/profile.py:210 +msgid "Not a valid ssh public key" +msgstr "SSH密钥不合法" + +#: users/forms/profile.py:170 users/models/user.py:708 +msgid "Public key" +msgstr "SSH公钥" + +#: users/models/user.py:561 +msgid "Force enable" +msgstr "强制启用" + +#: users/models/user.py:631 +msgid "Local" +msgstr "数据库" + +#: users/models/user.py:687 users/serializers/user.py:160 +msgid "Is service account" +msgstr "服务账号" + +#: users/models/user.py:689 +msgid "Avatar" +msgstr "头像" + +#: users/models/user.py:692 +msgid "Wechat" +msgstr "微信" + +#: users/models/user.py:695 +msgid "Phone" +msgstr "手机" + +#: users/models/user.py:701 +msgid "OTP secret key" +msgstr "OTP 秘钥" + +#: users/models/user.py:705 +msgid "Private key" +msgstr "ssh私钥" + +#: users/models/user.py:711 +msgid "Secret key" +msgstr "Secret key" + +#: users/models/user.py:716 users/serializers/profile.py:149 +#: users/serializers/user.py:157 +msgid "Is first login" +msgstr "首次登录" + +#: users/models/user.py:727 +msgid "Source" +msgstr "来源" + +#: users/models/user.py:731 +msgid "Date password last updated" +msgstr "最后更新密码日期" + +#: users/models/user.py:734 +msgid "Need update password" +msgstr "需要更新密码" + +#: users/models/user.py:902 +msgid "Can invite user" +msgstr "可以邀请用户" + +#: users/models/user.py:903 +msgid "Can remove user" +msgstr "可以移除用户" + +#: users/models/user.py:904 +msgid "Can match user" +msgstr "可以匹配用户" + +#: users/models/user.py:913 +msgid "Administrator" +msgstr "管理员" + +#: users/models/user.py:916 +msgid "Administrator is the super user of system" +msgstr "Administrator是初始的超级管理员" + +#: users/models/user.py:941 +msgid "User password history" +msgstr "用户密码历史" + +#: users/notifications.py:55 +#: users/templates/users/_msg_password_expire_reminder.html:17 +#: users/templates/users/reset_password.html:5 +#: users/templates/users/reset_password.html:6 +msgid "Reset password" +msgstr "重置密码" + +#: users/notifications.py:85 users/views/profile/reset.py:194 +msgid "Reset password success" +msgstr "重置密码成功" + +#: users/notifications.py:117 +msgid "Reset public key success" +msgstr "重置公钥成功" + +#: users/notifications.py:143 +msgid "Password is about expire" +msgstr "密码即将过期" + +#: users/notifications.py:171 +msgid "Account is about expire" +msgstr "账号即将过期" + +#: users/notifications.py:193 +msgid "Reset SSH Key" +msgstr "重置 SSH 密钥" + +#: users/notifications.py:214 +msgid "Reset MFA" +msgstr "重置 MFA" + +#: users/serializers/profile.py:30 +msgid "The old password is incorrect" +msgstr "旧密码错误" + +#: users/serializers/profile.py:37 users/serializers/profile.py:197 +msgid "Password does not match security rules" +msgstr "密码不满足安全规则" + +#: users/serializers/profile.py:41 +msgid "The new password cannot be the last {} passwords" +msgstr "新密码不能是最近 {} 次的密码" + +#: users/serializers/profile.py:49 users/serializers/profile.py:71 +msgid "The newly set password is inconsistent" +msgstr "两次密码不一致" + +#: users/serializers/user.py:31 +msgid "System roles" +msgstr "系统角色" + +#: users/serializers/user.py:35 +msgid "Org roles" +msgstr "组织角色" + +#: users/serializers/user.py:84 +msgid "Password strategy" +msgstr "密码策略" + +#: users/serializers/user.py:86 +msgid "MFA enabled" +msgstr "MFA 已启用" + +#: users/serializers/user.py:88 +msgid "MFA force enabled" +msgstr "强制 MFA" + +#: users/serializers/user.py:90 +msgid "Login blocked" +msgstr "登录被阻塞" + +#: users/serializers/user.py:93 +msgid "Can public key authentication" +msgstr "能否公钥认证" + +#: users/serializers/user.py:162 +msgid "Avatar url" +msgstr "头像路径" + +#: users/serializers/user.py:165 +msgid "Is OTP bound" +msgstr "是否绑定了虚拟 MFA" + +#: users/serializers/user.py:272 +msgid "Select users" +msgstr "选择用户" + +#: users/serializers/user.py:273 +msgid "For security, only list several users" +msgstr "为了安全,仅列出几个用户" + +#: users/serializers/user.py:307 +msgid "name not unique" +msgstr "名称重复" + +#: users/templates/users/_msg_account_expire_reminder.html:7 +msgid "Your account will expire in" +msgstr "您的账号即将过期" + +#: users/templates/users/_msg_account_expire_reminder.html:8 +msgid "" +"In order not to affect your normal work, please contact the administrator " +"for confirmation." +msgstr "" +"为了不影响您正常工作,请联系管理员确认。\n" +" " + +#: users/templates/users/_msg_password_expire_reminder.html:7 +msgid "Your password will expire in" +msgstr "您的密码将过期" + +#: users/templates/users/_msg_password_expire_reminder.html:8 +msgid "" +"For your account security, please click on the link below to update your " +"password in time" +msgstr "为了您的账号安全,请点击下面的链接及时更新密码" + +#: users/templates/users/_msg_password_expire_reminder.html:11 +msgid "Click here update password" +msgstr "点击这里更新密码" + +#: users/templates/users/_msg_password_expire_reminder.html:16 +msgid "If your password has expired, please click the link below to" +msgstr "如果你的密码已过期,请点击" + +#: users/templates/users/_msg_reset_mfa.html:7 +msgid "Your MFA has been reset by site administrator" +msgstr "你的 MFA 已经被管理员重置" + +#: users/templates/users/_msg_reset_mfa.html:8 +#: users/templates/users/_msg_reset_ssh_key.html:8 +msgid "Please click the link below to set" +msgstr "请点击下面链接设置" + +#: users/templates/users/_msg_reset_mfa.html:11 +#: users/templates/users/_msg_reset_ssh_key.html:11 +msgid "Click here set" +msgstr "点击这里设置" + +#: users/templates/users/_msg_reset_ssh_key.html:7 +msgid "Your ssh public key has been reset by site administrator" +msgstr "你的 SSH 密钥已经被管理员重置" + +#: users/templates/users/_msg_user_created.html:15 +msgid "click here to set your password" +msgstr "点击这里设置密码" + +#: users/templates/users/forgot_password.html:32 +msgid "Input your email account, that will send a email to your" +msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" + +#: users/templates/users/forgot_password.html:35 +msgid "" +"Enter your mobile number and a verification code will be sent to your phone" +msgstr "输入您的手机号码,验证码将发送到您的手机" + +#: users/templates/users/forgot_password.html:57 +msgid "Email account" +msgstr "邮箱账号" + +#: users/templates/users/forgot_password.html:61 +msgid "Mobile number" +msgstr "手机号码" + +#: users/templates/users/forgot_password.html:68 +msgid "Send" +msgstr "发送" + +#: users/templates/users/forgot_password.html:72 +#: users/templates/users/forgot_password_previewing.html:30 +msgid "Submit" +msgstr "提交" + +#: users/templates/users/forgot_password_previewing.html:21 +msgid "Please enter the username for which you want to retrieve the password" +msgstr "请输入您需要找回密码的用户名" + +#: users/templates/users/mfa_setting.html:24 +msgid "Enable MFA" +msgstr "启用 MFA 多因子认证" + +#: users/templates/users/mfa_setting.html:30 +msgid "MFA force enable, cannot disable" +msgstr "MFA 已强制启用,无法禁用" + +#: users/templates/users/mfa_setting.html:48 +msgid "MFA setting" +msgstr "设置 MFA 多因子认证" + +#: users/templates/users/reset_password.html:23 +msgid "Your password must satisfy" +msgstr "您的密码必须满足:" + +#: users/templates/users/reset_password.html:24 +msgid "Password strength" +msgstr "密码强度:" + +#: users/templates/users/reset_password.html:48 +msgid "Very weak" +msgstr "很弱" + +#: users/templates/users/reset_password.html:49 +msgid "Weak" +msgstr "弱" + +#: users/templates/users/reset_password.html:51 +msgid "Medium" +msgstr "一般" + +#: users/templates/users/reset_password.html:52 +msgid "Strong" +msgstr "强" + +#: users/templates/users/reset_password.html:53 +msgid "Very strong" +msgstr "很强" + +#: users/templates/users/user_otp_check_password.html:6 +msgid "Enable OTP" +msgstr "启用 MFA(OTP)" + +#: users/templates/users/user_otp_enable_bind.html:6 +msgid "Bind one-time password authenticator" +msgstr "绑定MFA验证器" + +#: users/templates/users/user_otp_enable_bind.html:13 +msgid "" +"Use the MFA Authenticator application to scan the following qr code for a 6-" +"bit verification code" +msgstr "使用 MFA 验证器应用扫描以下二维码,获取6位验证码" + +#: users/templates/users/user_otp_enable_bind.html:22 +#: users/templates/users/user_verify_mfa.html:27 +msgid "Six figures" +msgstr "6 位数字" + +#: users/templates/users/user_otp_enable_install_app.html:6 +msgid "Install app" +msgstr "安装应用" + +#: users/templates/users/user_otp_enable_install_app.html:13 +msgid "" +"Download and install the MFA Authenticator application on your phone or " +"applet of WeChat" +msgstr "请在手机端或微信小程序下载并安装 MFA 验证器应用" + +#: users/templates/users/user_otp_enable_install_app.html:18 +msgid "Android downloads" +msgstr "Android手机下载" + +#: users/templates/users/user_otp_enable_install_app.html:23 +msgid "iPhone downloads" +msgstr "iPhone手机下载" + +#: users/templates/users/user_otp_enable_install_app.html:26 +msgid "" +"After installation, click the next step to enter the binding page (if " +"installed, go to the next step directly)." +msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)" + +#: users/templates/users/user_password_verify.html:8 +#: users/templates/users/user_password_verify.html:9 +msgid "Verify password" +msgstr "校验密码" + +#: users/templates/users/user_verify_mfa.html:9 +msgid "Authenticate" +msgstr "验证身份" + +#: users/templates/users/user_verify_mfa.html:15 +msgid "" +"The account protection has been opened, please complete the following " +"operations according to the prompts" +msgstr "账号保护已开启,请根据提示完成以下操作" + +#: users/templates/users/user_verify_mfa.html:17 +msgid "Open MFA Authenticator and enter the 6-bit dynamic code" +msgstr "请打开 MFA 验证器,输入 6 位动态码" + +#: users/views/profile/otp.py:87 +msgid "Already bound" +msgstr "已经绑定" + +#: users/views/profile/otp.py:88 +msgid "MFA already bound, disable first, then bound" +msgstr "MFA(OTP) 已经绑定,请先禁用,再绑定" + +#: users/views/profile/otp.py:115 +msgid "OTP enable success" +msgstr "MFA(OTP) 启用成功" + +#: users/views/profile/otp.py:116 +msgid "OTP enable success, return login page" +msgstr "MFA(OTP) 启用成功,返回到登录页面" + +#: users/views/profile/otp.py:158 +msgid "Disable OTP" +msgstr "禁用虚拟 MFA(OTP)" + +#: users/views/profile/otp.py:164 +msgid "OTP disable success" +msgstr "MFA(OTP) 禁用成功" + +#: users/views/profile/otp.py:165 +msgid "OTP disable success, return login page" +msgstr "MFA(OTP) 禁用成功,返回登录页面" + +#: users/views/profile/password.py:36 users/views/profile/password.py:41 +msgid "Password invalid" +msgstr "用户名或密码无效" + +#: users/views/profile/reset.py:47 +msgid "" +"Non-local users can log in only from third-party platforms and cannot change " +"their passwords: {}" +msgstr "非本地用户仅允许从第三方平台登录,不支持修改密码: {}" + +#: users/views/profile/reset.py:149 users/views/profile/reset.py:160 +msgid "Token invalid or expired" +msgstr "令牌错误或失效" + +#: users/views/profile/reset.py:165 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + +#: users/views/profile/reset.py:172 +msgid "* Your password does not meet the requirements" +msgstr "* 您的密码不符合要求" + +#: users/views/profile/reset.py:178 +msgid "* The new password cannot be the last {} passwords" +msgstr "* 新密码不能是最近 {} 次的密码" + +#: users/views/profile/reset.py:195 +msgid "Reset password success, return to login page" +msgstr "重置密码成功,返回到登录页面" + +#: xpack/apps.py:8 +msgid "XPACK" +msgstr "XPack" + +#: xpack/plugins/cloud/api.py:40 +msgid "Test connection successful" +msgstr "测试成功" + +#: xpack/plugins/cloud/api.py:42 +msgid "Test connection failed: {}" +msgstr "测试连接失败:{}" + +#: xpack/plugins/cloud/const.py:8 +msgid "Alibaba Cloud" +msgstr "阿里云" + +#: xpack/plugins/cloud/const.py:9 +msgid "AWS (International)" +msgstr "AWS (国际)" + +#: xpack/plugins/cloud/const.py:10 +msgid "AWS (China)" +msgstr "AWS (中国)" + +#: xpack/plugins/cloud/const.py:11 +msgid "Azure (China)" +msgstr "Azure (中国)" + +#: xpack/plugins/cloud/const.py:12 +msgid "Azure (International)" +msgstr "Azure (国际)" + +#: xpack/plugins/cloud/const.py:14 +msgid "Baidu Cloud" +msgstr "百度云" + +#: xpack/plugins/cloud/const.py:15 +msgid "JD Cloud" +msgstr "京东云" + +#: xpack/plugins/cloud/const.py:16 +msgid "KingSoft Cloud" +msgstr "金山云" + +#: xpack/plugins/cloud/const.py:17 +msgid "Tencent Cloud" +msgstr "腾讯云" + +#: xpack/plugins/cloud/const.py:18 +msgid "Tencent Cloud (Lighthouse)" +msgstr "腾讯云(轻量服务器应用)" + +#: xpack/plugins/cloud/const.py:19 +msgid "VMware" +msgstr "VMware" + +#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 +msgid "Nutanix" +msgstr "Nutanix" + +#: xpack/plugins/cloud/const.py:21 +msgid "Huawei Private Cloud" +msgstr "华为私有云" + +#: xpack/plugins/cloud/const.py:22 +msgid "Qingyun Private Cloud" +msgstr "青云私有云" + +#: xpack/plugins/cloud/const.py:23 +msgid "CTYun Private Cloud" +msgstr "天翼私有云" + +#: xpack/plugins/cloud/const.py:24 +msgid "OpenStack" +msgstr "OpenStack" + +#: xpack/plugins/cloud/const.py:25 +msgid "Google Cloud Platform" +msgstr "谷歌云" + +#: xpack/plugins/cloud/const.py:26 +msgid "Fusion Compute" +msgstr "" + +#: xpack/plugins/cloud/const.py:31 +msgid "Private IP" +msgstr "私有IP" + +#: xpack/plugins/cloud/const.py:32 +msgid "Public IP" +msgstr "公网IP" + +#: xpack/plugins/cloud/const.py:36 +msgid "Instance name" +msgstr "实例名称" + +#: xpack/plugins/cloud/const.py:37 +msgid "Instance name and Partial IP" +msgstr "实例名称和部分IP" + +#: xpack/plugins/cloud/const.py:42 +msgid "Succeed" +msgstr "成功" + +#: xpack/plugins/cloud/const.py:46 +msgid "Unsync" +msgstr "未同步" + +#: xpack/plugins/cloud/const.py:47 +msgid "New Sync" +msgstr "新同步" + +#: xpack/plugins/cloud/const.py:48 +msgid "Synced" +msgstr "已同步" + +#: xpack/plugins/cloud/const.py:49 +msgid "Released" +msgstr "已释放" + +#: xpack/plugins/cloud/meta.py:9 +msgid "Cloud center" +msgstr "云管中心" + +#: xpack/plugins/cloud/models.py:30 +msgid "Provider" +msgstr "云服务商" + +#: xpack/plugins/cloud/models.py:34 +msgid "Validity" +msgstr "有效" + +#: xpack/plugins/cloud/models.py:39 +msgid "Cloud account" +msgstr "云账号" + +#: xpack/plugins/cloud/models.py:41 +msgid "Test cloud account" +msgstr "测试云账号" + +#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37 +msgid "Regions" +msgstr "地域" + +#: xpack/plugins/cloud/models.py:91 +msgid "Hostname strategy" +msgstr "主机名策略" + +#: xpack/plugins/cloud/models.py:100 +msgid "Unix admin user" +msgstr "Unix 管理员" + +#: xpack/plugins/cloud/models.py:104 +msgid "Windows admin user" +msgstr "Windows 管理员" + +#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:41 +msgid "IP network segment group" +msgstr "IP网段组" + +#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:46 +msgid "Sync IP type" +msgstr "同步IP类型" + +#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/serializers/task.py:64 +msgid "Always update" +msgstr "总是更新" + +#: xpack/plugins/cloud/models.py:122 +msgid "Date last sync" +msgstr "最后同步日期" + +#: xpack/plugins/cloud/models.py:127 xpack/plugins/cloud/models.py:168 +msgid "Sync instance task" +msgstr "同步实例任务" + +#: xpack/plugins/cloud/models.py:179 xpack/plugins/cloud/models.py:227 +msgid "Date sync" +msgstr "同步日期" + +#: xpack/plugins/cloud/models.py:183 +msgid "Sync instance task execution" +msgstr "同步实例任务执行" + +#: xpack/plugins/cloud/models.py:207 +msgid "Sync task" +msgstr "同步任务" + +#: xpack/plugins/cloud/models.py:211 +msgid "Sync instance task history" +msgstr "同步实例任务历史" + +#: xpack/plugins/cloud/models.py:214 +msgid "Instance" +msgstr "实例" + +#: xpack/plugins/cloud/models.py:231 +msgid "Sync instance detail" +msgstr "同步实例详情" + +#: xpack/plugins/cloud/providers/aws_international.py:17 +msgid "China (Beijing)" +msgstr "中国(北京)" + +#: xpack/plugins/cloud/providers/aws_international.py:18 +msgid "China (Ningxia)" +msgstr "中国(宁夏)" + +#: xpack/plugins/cloud/providers/aws_international.py:21 +msgid "US East (Ohio)" +msgstr "美国东部(俄亥俄州)" + +#: xpack/plugins/cloud/providers/aws_international.py:22 +msgid "US East (N. Virginia)" +msgstr "美国东部(弗吉尼亚北部)" + +#: xpack/plugins/cloud/providers/aws_international.py:23 +msgid "US West (N. California)" +msgstr "美国西部(加利福尼亚北部)" + +#: xpack/plugins/cloud/providers/aws_international.py:24 +msgid "US West (Oregon)" +msgstr "美国西部(俄勒冈)" + +#: xpack/plugins/cloud/providers/aws_international.py:25 +msgid "Africa (Cape Town)" +msgstr "非洲(开普敦)" + +#: xpack/plugins/cloud/providers/aws_international.py:26 +msgid "Asia Pacific (Hong Kong)" +msgstr "亚太地区(香港)" + +#: xpack/plugins/cloud/providers/aws_international.py:27 +msgid "Asia Pacific (Mumbai)" +msgstr "亚太地区(孟买)" + +#: xpack/plugins/cloud/providers/aws_international.py:28 +msgid "Asia Pacific (Osaka-Local)" +msgstr "亚太区域(大阪当地)" + +#: xpack/plugins/cloud/providers/aws_international.py:29 +msgid "Asia Pacific (Seoul)" +msgstr "亚太区域(首尔)" + +#: xpack/plugins/cloud/providers/aws_international.py:30 +msgid "Asia Pacific (Singapore)" +msgstr "亚太区域(新加坡)" + +#: xpack/plugins/cloud/providers/aws_international.py:31 +msgid "Asia Pacific (Sydney)" +msgstr "亚太区域(悉尼)" + +#: xpack/plugins/cloud/providers/aws_international.py:32 +msgid "Asia Pacific (Tokyo)" +msgstr "亚太区域(东京)" + +#: xpack/plugins/cloud/providers/aws_international.py:33 +msgid "Canada (Central)" +msgstr "加拿大(中部)" + +#: xpack/plugins/cloud/providers/aws_international.py:34 +msgid "Europe (Frankfurt)" +msgstr "欧洲(法兰克福)" + +#: xpack/plugins/cloud/providers/aws_international.py:35 +msgid "Europe (Ireland)" +msgstr "欧洲(爱尔兰)" + +#: xpack/plugins/cloud/providers/aws_international.py:36 +msgid "Europe (London)" +msgstr "欧洲(伦敦)" + +#: xpack/plugins/cloud/providers/aws_international.py:37 +msgid "Europe (Milan)" +msgstr "欧洲(米兰)" + +#: xpack/plugins/cloud/providers/aws_international.py:38 +msgid "Europe (Paris)" +msgstr "欧洲(巴黎)" + +#: xpack/plugins/cloud/providers/aws_international.py:39 +msgid "Europe (Stockholm)" +msgstr "欧洲(斯德哥尔摩)" + +#: xpack/plugins/cloud/providers/aws_international.py:40 +msgid "Middle East (Bahrain)" +msgstr "中东(巴林)" + +#: xpack/plugins/cloud/providers/aws_international.py:41 +msgid "South America (São Paulo)" +msgstr "南美洲(圣保罗)" + +#: xpack/plugins/cloud/providers/baiducloud.py:54 +#: xpack/plugins/cloud/providers/jdcloud.py:127 +msgid "CN North-Beijing" +msgstr "华北-北京" + +#: xpack/plugins/cloud/providers/baiducloud.py:55 +#: xpack/plugins/cloud/providers/huaweicloud.py:40 +#: xpack/plugins/cloud/providers/jdcloud.py:130 +msgid "CN South-Guangzhou" +msgstr "华南-广州" + +#: xpack/plugins/cloud/providers/baiducloud.py:56 +msgid "CN East-Suzhou" +msgstr "华东-苏州" + +#: xpack/plugins/cloud/providers/baiducloud.py:57 +#: xpack/plugins/cloud/providers/huaweicloud.py:48 +msgid "CN-Hong Kong" +msgstr "中国-香港" + +#: xpack/plugins/cloud/providers/baiducloud.py:58 +msgid "CN Center-Wuhan" +msgstr "华中-武汉" + +#: xpack/plugins/cloud/providers/baiducloud.py:59 +msgid "CN North-Baoding" +msgstr "华北-保定" + +#: xpack/plugins/cloud/providers/baiducloud.py:60 +#: xpack/plugins/cloud/providers/jdcloud.py:129 +msgid "CN East-Shanghai" +msgstr "华东-上海" + +#: xpack/plugins/cloud/providers/baiducloud.py:61 +#: xpack/plugins/cloud/providers/huaweicloud.py:47 +msgid "AP-Singapore" +msgstr "亚太-新加坡" + +#: xpack/plugins/cloud/providers/huaweicloud.py:35 +msgid "AF-Johannesburg" +msgstr "非洲-约翰内斯堡" + +#: xpack/plugins/cloud/providers/huaweicloud.py:36 +msgid "CN North-Beijing4" +msgstr "华北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:37 +msgid "CN North-Beijing1" +msgstr "华北-北京1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:38 +msgid "CN East-Shanghai2" +msgstr "华东-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:39 +msgid "CN East-Shanghai1" +msgstr "华东-上海1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:41 +msgid "LA-Mexico City1" +msgstr "拉美-墨西哥城一" + +#: xpack/plugins/cloud/providers/huaweicloud.py:42 +msgid "LA-Santiago" +msgstr "拉美-圣地亚哥" + +#: xpack/plugins/cloud/providers/huaweicloud.py:43 +msgid "LA-Sao Paulo1" +msgstr "拉美-圣保罗一" + +#: xpack/plugins/cloud/providers/huaweicloud.py:44 +msgid "EU-Paris" +msgstr "欧洲-巴黎" + +#: xpack/plugins/cloud/providers/huaweicloud.py:45 +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:46 +msgid "AP-Bangkok" +msgstr "亚太-曼谷" + +#: xpack/plugins/cloud/providers/huaweicloud.py:50 +msgid "CN Northeast-Dalian" +msgstr "华北-大连" + +#: xpack/plugins/cloud/providers/huaweicloud.py:51 +msgid "CN North-Ulanqab1" +msgstr "华北-乌兰察布一" + +#: xpack/plugins/cloud/providers/huaweicloud.py:52 +msgid "CN South-Guangzhou-InvitationOnly" +msgstr "华南-广州-友好用户环境" + +#: xpack/plugins/cloud/providers/jdcloud.py:128 +msgid "CN East-Suqian" +msgstr "华东-宿迁" + +#: xpack/plugins/cloud/serializers/account.py:63 +msgid "Validity display" +msgstr "有效性显示" + +#: xpack/plugins/cloud/serializers/account.py:64 +msgid "Provider display" +msgstr "服务商显示" + +#: xpack/plugins/cloud/serializers/account_attrs.py:35 +msgid "Client ID" +msgstr "客户端 ID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:41 +msgid "Tenant ID" +msgstr "租户 ID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:44 +msgid "Subscription ID" +msgstr "订阅 ID" + +#: xpack/plugins/cloud/serializers/account_attrs.py:95 +#: xpack/plugins/cloud/serializers/account_attrs.py:100 +#: xpack/plugins/cloud/serializers/account_attrs.py:116 +#: xpack/plugins/cloud/serializers/account_attrs.py:141 +msgid "API Endpoint" +msgstr "API 端点" + +#: xpack/plugins/cloud/serializers/account_attrs.py:106 +msgid "Auth url" +msgstr "认证地址" + +#: xpack/plugins/cloud/serializers/account_attrs.py:107 +msgid "eg: http://openstack.example.com:5000/v3" +msgstr "如: http://openstack.example.com:5000/v3" + +#: xpack/plugins/cloud/serializers/account_attrs.py:110 +msgid "User domain" +msgstr "用户域" + +#: xpack/plugins/cloud/serializers/account_attrs.py:117 +msgid "Cert File" +msgstr "证书文件" + +#: xpack/plugins/cloud/serializers/account_attrs.py:118 +msgid "Key File" +msgstr "秘钥文件" + +#: xpack/plugins/cloud/serializers/account_attrs.py:134 +msgid "Service account key" +msgstr "服务账号密钥" + +#: xpack/plugins/cloud/serializers/account_attrs.py:135 +msgid "The file is in JSON format" +msgstr "JSON 格式的文件" + +#: xpack/plugins/cloud/serializers/account_attrs.py:148 +msgid "IP address invalid `{}`, {}" +msgstr "IP 地址无效: `{}`, {}" + +#: xpack/plugins/cloud/serializers/account_attrs.py:154 +msgid "" +"Format for comma-delimited string,Such as: 192.168.1.0/24, " +"10.0.0.0-10.0.0.255" +msgstr "格式为逗号分隔的字符串,如:192.168.1.0/24,10.0.0.0-10.0.0.255" + +#: xpack/plugins/cloud/serializers/account_attrs.py:158 +msgid "" +"The port is used to detect the validity of the IP address. When the " +"synchronization task is executed, only the valid IP address will be " +"synchronized.
If the port is 0, all IP addresses are valid." +msgstr "" +"端口用来检测 IP 地址的有效性,在同步任务执行时,只会同步有效的 IP 地址。
" +"如果端口为 0,则表示所有 IP 地址均有效。" + +#: xpack/plugins/cloud/serializers/account_attrs.py:166 +msgid "Hostname prefix" +msgstr "主机名前缀" + +#: xpack/plugins/cloud/serializers/account_attrs.py:169 +msgid "IP segment" +msgstr "IP 网段" + +#: xpack/plugins/cloud/serializers/account_attrs.py:173 +msgid "Test port" +msgstr "测试端口" + +#: xpack/plugins/cloud/serializers/account_attrs.py:176 +msgid "Test timeout" +msgstr "测试超时时间" + +#: xpack/plugins/cloud/serializers/task.py:28 +msgid "" +"Only instances matching the IP range will be synced.
If the instance " +"contains multiple IP addresses, the first IP address that matches will be " +"used as the IP for the created asset.
The default value of * means sync " +"all instances and randomly match IP addresses.
Format for comma-" +"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20" +msgstr "" +"只有匹配到 IP 段的实例会被同步。
如果实例包含多个 IP 地址,那么第一个匹配" +"到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 " +"IP 地址。
格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20" + +#: xpack/plugins/cloud/serializers/task.py:35 +msgid "History count" +msgstr "执行次数" + +#: xpack/plugins/cloud/serializers/task.py:36 +msgid "Instance count" +msgstr "实例个数" + +#: xpack/plugins/cloud/serializers/task.py:63 +msgid "Linux admin user" +msgstr "Linux 管理员" + +#: xpack/plugins/cloud/utils.py:69 +msgid "Account unavailable" +msgstr "账号无效" + +#: xpack/plugins/interface/api.py:52 +msgid "Restore default successfully." +msgstr "恢复默认成功!" + +#: xpack/plugins/interface/meta.py:10 +msgid "Interface settings" +msgstr "界面设置" + +#: xpack/plugins/interface/models.py:22 +msgid "Title of login page" +msgstr "登录页面标题" + +#: xpack/plugins/interface/models.py:26 +msgid "Image of login page" +msgstr "登录页面图片" + +#: xpack/plugins/interface/models.py:30 +msgid "Website icon" +msgstr "网站图标" + +#: xpack/plugins/interface/models.py:34 +msgid "Logo of management page" +msgstr "管理页面logo" + +#: xpack/plugins/interface/models.py:38 +msgid "Logo of logout page" +msgstr "退出页面logo" + +#: xpack/plugins/interface/models.py:40 +msgid "Theme" +msgstr "主题" + +#: xpack/plugins/interface/models.py:43 xpack/plugins/interface/models.py:84 +msgid "Interface setting" +msgstr "界面设置" + +#: xpack/plugins/license/api.py:50 +msgid "License import successfully" +msgstr "许可证导入成功" + +#: xpack/plugins/license/api.py:51 +msgid "License is invalid" +msgstr "无效的许可证" + +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +msgid "License" +msgstr "许可证" + +#: xpack/plugins/license/models.py:71 +msgid "Standard edition" +msgstr "标准版" + +#: xpack/plugins/license/models.py:73 +msgid "Enterprise edition" +msgstr "企业版" + +#: xpack/plugins/license/models.py:75 +msgid "Ultimate edition" +msgstr "旗舰版" + +#: xpack/plugins/license/models.py:77 +msgid "Community edition" +msgstr "社区版" + +#~ msgid "Welcome back, please enter username and password to login" +#~ msgstr "欢迎回来,请输入用户名和密码登录" + +#~ msgid "Discovery account automation" +#~ msgstr "账号发现" + +#~ msgid "Category display" +#~ msgstr "类别名称" + +#~ msgid "Built-in" +#~ msgstr "内置" + +#~ msgid "Scope display" +#~ msgstr "范围名称" + +#~ msgid "App change auth" +#~ msgstr "应用改密" + +#~ msgid "My apps" +#~ msgstr "我的应用" + +#~ msgid "Risk level display" +#~ msgstr "风险等级名称" + +#~ msgid "From cmd filter" +#~ msgstr "来自命令过滤规则" + +#~ msgid "From cmd filter rule" +#~ msgstr "来自命令过滤规则" + +#~ msgid "System roles display" +#~ msgstr "系统角色显示" + +#~ msgid "Org roles display" +#~ msgstr "组织角色显示" + +#~ msgid "MFA level display" +#~ msgstr "MFA 等级名称" + +#~ msgid "Groups name" +#~ msgstr "用户组名" + +#~ msgid "Source name" +#~ msgstr "用户来源名" + +#~ msgid "Organization role name" +#~ msgstr "组织角色名称" + +#~ msgid "Super role name" +#~ msgstr "超级角色名称" + +#~ msgid "Total role name" +#~ msgstr "汇总角色名称" + +#~ msgid "Is wecom bound" +#~ msgstr "是否绑定了企业微信" + +#~ msgid "Is dingtalk bound" +#~ msgstr "是否绑定了钉钉" + +#~ msgid "Is feishu bound" +#~ msgstr "是否绑定了飞书" + +#~ msgid "System role name" +#~ msgstr "系统角色名称" + +#~ msgid "Change auth plan" +#~ msgstr "改密计划" + +#~ msgid "Application change auth plan" +#~ msgstr "应用改密计划" + +#~ msgid "Application change auth plan execution" +#~ msgstr "应用改密计划执行" + +#~ msgid "App" +#~ msgstr "应用" + +#~ msgid "Application change auth plan task" +#~ msgstr "应用改密计划任务" + +#~ msgid "Password cannot be set to blank, exit. " +#~ msgstr "密码不能设置为空, 退出. " + +#~ msgid "Asset change auth plan" +#~ msgstr "资产改密计划" + +#~ msgid "Asset change auth plan execution" +#~ msgstr "资产改密计划执行" + +#~ msgid "Change auth plan execution" +#~ msgstr "改密计划执行" + +#~ msgid "Asset change auth plan task" +#~ msgstr "资产改密计划任务" + +#~ msgid "This asset does not have a privileged user set: " +#~ msgstr "该资产没有设置特权用户: " + +#~ msgid "" +#~ "The password and key of the current asset privileged user cannot be " +#~ "changed: " +#~ msgstr "不能更改当前资产特权用户的密码及密钥: " + +#~ msgid "Public key cannot be set to null, exit. " +#~ msgstr "公钥不能设置为空, 退出. " + +#~ msgid "Change auth plan snapshot" +#~ msgstr "改密计划快照" + +#~ msgid "Preflight check" +#~ msgstr "改密前的校验" + +#~ msgid "Change auth" +#~ msgstr "执行改密" + +#~ msgid "Verify auth" +#~ msgstr "验证密码/密钥" + +#~ msgid "Keep auth" +#~ msgstr "保存密码/密钥" + +#~ msgid "Step" +#~ msgstr "步骤" + +#~ msgid "Change Password" +#~ msgstr "更改密码" + +#~ msgid "Change SSH Key" +#~ msgstr "修改 SSH Key" + +#~ msgid "Run times" +#~ msgstr "执行次数" + +#~ msgid "After many attempts to change the secret, it still failed" +#~ msgstr "多次尝试改密后, 依然失败" + +#~ msgid "Invalid/incorrect password" +#~ msgstr "无效/错误 密码" + +#~ msgid "Failed to connect to the host" +#~ msgstr "连接主机失败" + +#~ msgid "Data could not be sent to remote" +#~ msgstr "无法将数据发送到远程" + +#~ msgid "Periodic display" +#~ msgstr "定时执行" + +#~ msgid "Gathered user" +#~ msgstr "收集用户" + +#~ msgid "Gather user task" +#~ msgstr "收集用户任务" + +#~ msgid "gather user task execution" +#~ msgstr "收集用户执行" + +#~ msgid "Assets is empty, please change nodes" +#~ msgstr "资产为空,请更改节点" + +#~ msgid "Executed times" +#~ msgstr "执行次数" + +#~ msgid "System User" +#~ msgstr "系统用户" + +#~ msgid "" +#~ "Format for comma-delimited string, with * indicating a match all. " +#~ "Protocol options: {}" +#~ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" + +#~ msgid "Unsupported protocols: {}" +#~ msgstr "不支持的协议: {}" + +#~ msgid "Remote app" +#~ msgstr "远程应用" + +#~ msgid "Custom" +#~ msgstr "自定义" + +#~ msgid "Can view application account secret" +#~ msgstr "可以查看应用账号密码" + +#~ msgid "Can change application account secret" +#~ msgstr "可以查看应用账号密码" + +#~ msgid "Application user" +#~ msgstr "应用用户" + +#~ msgid "Type display" +#~ msgstr "类型名称" + +#~ msgid "Application display" +#~ msgstr "应用名称" + +#~ msgid "Cluster" +#~ msgstr "集群" + +#~ msgid "CA certificate" +#~ msgstr "CA 证书" + +#~ msgid "Client certificate file" +#~ msgstr "客户端证书" + +#~ msgid "Certificate key file" +#~ msgstr "证书秘钥" + +#~ msgid "Application path" +#~ msgstr "应用路径" + +#~ msgid "Target URL" +#~ msgstr "目标URL" + +#~ msgid "Chrome username" +#~ msgstr "Chrome 用户名" + +#~ msgid "Chrome password" +#~ msgstr "Chrome 密码" + +#~ msgid "Operating parameter" +#~ msgstr "运行参数" + +#~ msgid "Target url" +#~ msgstr "目标URL" + +#~ msgid "Mysql workbench username" +#~ msgstr "Mysql 工作台 用户名" + +#~ msgid "Mysql workbench password" +#~ msgstr "Mysql 工作台 密码" + +#~ msgid "Vmware username" +#~ msgstr "Vmware 用户名" + +#~ msgid "Vmware password" +#~ msgstr "Vmware 密码" + +#~ msgid "Base" +#~ msgstr "基础" + +#~ msgid "Can test asset account connectivity" +#~ msgstr "可以测试资产账号连接性" + +#~ msgid "Bandwidth" +#~ msgstr "带宽" + +#~ msgid "Contact" +#~ msgstr "联系人" + +#~ msgid "Intranet" +#~ msgstr "内网" + +#~ msgid "Extranet" +#~ msgstr "外网" + +#~ msgid "Operator" +#~ msgstr "运营商" + +#~ msgid "Default Cluster" +#~ msgstr "默认Cluster" + +#~ msgid "Test gateway" +#~ msgstr "测试网关" + +#~ msgid "User groups" +#~ msgstr "用户组" + +#~ msgid "System user display" +#~ msgstr "系统用户名称" + +#~ msgid "Protocol format should {}/{}" +#~ msgstr "协议格式 {}/{}" + +#~ msgid "Nodes name" +#~ msgstr "节点名称" + +#~ msgid "Labels name" +#~ msgstr "标签名称" + +#~ msgid "Hardware info" +#~ msgstr "硬件信息" + +#~ msgid "Admin user display" +#~ msgstr "特权用户名称" + +#~ msgid "CPU info" +#~ msgstr "CPU信息" + +#~ msgid "Action display" +#~ msgstr "动作名称" + +#~ msgid "Applications amount" +#~ msgstr "应用数量" + +#~ msgid "Gateways count" +#~ msgstr "网关数量" + +#~ msgid "SSH key fingerprint" +#~ msgstr "密钥指纹" + +#~ msgid "Apps amount" +#~ msgstr "应用数量" + +#~ msgid "Login mode display" +#~ msgstr "认证方式名称" + +#~ msgid "Ad domain" +#~ msgstr "Ad 网域" + +#~ msgid "Is asset protocol" +#~ msgstr "资产协议" + +#~ msgid "Only ssh and automatic login system users are supported" +#~ msgstr "仅支持ssh协议和自动登录的系统用户" + +#~ msgid "Username same with user with protocol {} only allow 1" +#~ msgstr "用户名和用户相同的一种协议只允许存在一个" + +#~ msgid "* Automatic login mode must fill in the username." +#~ msgstr "自动登录模式,必须填写用户名" + +#~ msgid "Path should starts with /" +#~ msgstr "路径应该以 / 开头" + +#~ msgid "Password or private key required" +#~ msgstr "密码或密钥密码需要一个" + +#~ msgid "Only ssh protocol system users are allowed" +#~ msgstr "仅允许ssh协议的系统用户" + +#~ msgid "The protocol must be consistent with the current user: {}" +#~ msgstr "协议必须和当前用户保持一致: {}" + +#~ msgid "Only system users with automatic login are allowed" +#~ msgstr "仅允许自动登录的系统用户" + +#~ msgid "System user name" +#~ msgstr "系统用户名称" + +#~ msgid "Asset hostname" +#~ msgstr "资产主机名" + +#~ msgid "The asset {} system platform {} does not support run Ansible tasks" +#~ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" + +#~ msgid "Test assets connectivity: " +#~ msgstr "测试资产可连接性: " + +#~ msgid "Unreachable" +#~ msgstr "不可达" + +#~ msgid "Reachable" +#~ msgstr "可连接" + +#~ msgid "Get asset info failed: {}" +#~ msgstr "获取资产信息失败:{}" + +#~ msgid "Update asset hardware info: " +#~ msgstr "更新资产硬件信息: " + +#~ msgid "System user is dynamic: {}" +#~ msgstr "系统用户是动态的: {}" + +#~ msgid "Start push system user for platform: [{}]" +#~ msgstr "推送系统用户到平台: [{}]" + +#~ msgid "Hosts count: {}" +#~ msgstr "主机数量: {}" + +#~ msgid "Push system users to asset: " +#~ msgstr "推送系统用户到入资产: " + +#~ msgid "Dynamic system user not support test" +#~ msgstr "动态系统用户不支持测试" + +#~ msgid "Start test system user connectivity for platform: [{}]" +#~ msgstr "开始测试系统用户在该系统平台的可连接性: [{}]" + +#~ msgid "Test system user connectivity: " +#~ msgstr "测试系统用户可连接性: " + +#~ msgid "Test system user connectivity period: " +#~ msgstr "定期测试系统用户可连接性: " + +#~ msgid "Operate display" +#~ msgstr "操作名称" + +#~ msgid "Status display" +#~ msgstr "状态名称" + +#~ msgid "MFA display" +#~ msgstr "MFA名称" + +#~ msgid "Hosts display" +#~ msgstr "主机名称" + +#~ msgid "Run as" +#~ msgstr "运行用户" + +#~ msgid "Run as display" +#~ msgstr "运行用户名称" + +#~ msgid "User not exists" +#~ msgstr "用户不存在" + +#~ msgid "System user not exists" +#~ msgstr "系统用户不存在" + +#~ msgid "Asset not exists" +#~ msgstr "资产不存在" + +#~ msgid "User has no permission to access asset or permission expired" +#~ msgstr "用户没有权限访问资产或权限已过期" + +#~ msgid "User has no permission to access application or permission expired" +#~ msgstr "用户没有权限访问应用或权限已过期" + +#~ msgid "Asset or application required" +#~ msgstr "资产或应用必填" + +#~ msgid "Not has host {} permission" +#~ msgstr "没有该主机 {} 权限" + +#~ msgid "" +#~ "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " +#~ "crontab expressions (Online tools)
Note: If both Regularly " +#~ "perform and Cycle perform are set, give priority to Regularly perform" +#~ msgstr "" +#~ "eg:每周日 03:05 执行 <5 3 * * 0>
提示: 使用5位 Linux crontab 表达" +#~ "式 <分 时 日 月 星期> (在线工具
注意: 如果同时设置了定期执行和周期执" +#~ "行,优先使用定期执行" + +#~ msgid "Unit: hour" +#~ msgstr "单位: 时" + +#~ msgid "Callback" +#~ msgstr "回调" + +#~ msgid "Can view task monitor" +#~ msgstr "可以查看任务监控" + +#~ msgid "Tasks" +#~ msgstr "任务" + +#~ msgid "Run as admin" +#~ msgstr "再次执行" + +#~ msgid "Become" +#~ msgstr "Become" + +#~ msgid "Create by" +#~ msgstr "创建者" + +#~ msgid "Task display" +#~ msgstr "任务名称" + +#~ msgid "Host amount" +#~ msgstr "主机数量" + +#~ msgid "Start time" +#~ msgstr "开始时间" + +#~ msgid "End time" +#~ msgstr "完成时间" + +#~ msgid "Adhoc raw result" +#~ msgstr "结果" + +#~ msgid "Adhoc result summary" +#~ msgstr "汇总" + +#~ msgid "AdHoc execution" +#~ msgstr "任务执行" + +#~ msgid "Task start" +#~ msgstr "任务开始" + +#~ msgid "Command `{}` is forbidden ........" +#~ msgstr "命令 `{}` 不允许被执行 ......." + +#~ msgid "Task end" +#~ msgstr "任务结束" + +#~ msgid "Clean task history period" +#~ msgstr "定期清除任务历史" + +#~ msgid "The administrator is modifying permissions. Please wait" +#~ msgstr "管理员正在修改授权,请稍等" + +#~ msgid "The authorization cannot be revoked for the time being" +#~ msgstr "该授权暂时不能撤销" + +#~ msgid "Application permission" +#~ msgstr "应用授权" + +#~ msgid "Permed application" +#~ msgstr "授权的应用" + +#~ msgid "Can view my apps" +#~ msgstr "可以查看我的应用" + +#~ msgid "Can view user apps" +#~ msgstr "可以查看用户授权的应用" + +#~ msgid "Can view usergroup apps" +#~ msgstr "可以查看用户组授权的应用" + +#~ msgid "Upload file" +#~ msgstr "上传文件" + +#~ msgid "Download file" +#~ msgstr "下载文件" + +#~ msgid "Upload download" +#~ msgstr "上传下载" + +#~ msgid "Clipboard paste" +#~ msgstr "剪贴板粘贴" + +#~ msgid "Clipboard copy paste" +#~ msgstr "剪贴板复制粘贴" + +#~ msgid "Your permed applications is about to expire" +#~ msgstr "你授权的应用即将过期" + +#~ msgid "permed applications" +#~ msgstr "授权的应用" + +#~ msgid "Application permissions is about to expire" +#~ msgstr "应用授权规则即将过期" + +#~ msgid "application permissions of organization {}" +#~ msgstr "组织 ({}) 的应用授权" + +#~ msgid "User groups amount" +#~ msgstr "用户组数量" + +#~ msgid "System users amount" +#~ msgstr "系统用户数量" + +#~ msgid "" +#~ "The application list contains applications that are different from the " +#~ "permission type. ({})" +#~ msgstr "应用列表中包含与授权类型不同的应用。({})" + +#~ msgid "Users display" +#~ msgstr "用户名称" + +#~ msgid "User groups display" +#~ msgstr "用户组名称" + +#~ msgid "Assets display" +#~ msgstr "资产名称" + +#~ msgid "Nodes display" +#~ msgstr "节点名称" + +#~ msgid "System users display" +#~ msgstr "系统用户名称" + +#~ msgid "My applications" +#~ msgstr "我的应用" + +#~ msgid "Empty" +#~ msgstr "空" + +#~ msgid "System user ID" +#~ msgstr "系统用户 ID" + +#~ msgid "Apply for application" +#~ msgstr "申请应用" + +#~ msgid "" +#~ "Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " +#~ "processor: {}, ticket ID: {}" +#~ msgstr "" +#~ "通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" + +#~ msgid "Applied login IP" +#~ msgstr "申请登录的IP" + +#~ msgid "Applied login city" +#~ msgstr "申请登录的城市" + +#~ msgid "Applied login datetime" +#~ msgstr "申请登录的日期" + +#~ msgid "Login system user" +#~ msgstr "登录系统用户" diff --git a/apps/notifications/migrations/0003_auto_20221220_1956.py b/apps/notifications/migrations/0003_auto_20221220_1956.py new file mode 100644 index 000000000..7cbac3e18 --- /dev/null +++ b/apps/notifications/migrations/0003_auto_20221220_1956.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0002_auto_20210909_1946'), + ] + + operations = [ + migrations.AlterField( + model_name='sitemessage', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='sitemessage', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='sitemessageusers', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='sitemessageusers', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='systemmsgsubscription', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='systemmsgsubscription', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='usermsgsubscription', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='usermsgsubscription', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py index 24995e975..410d07a28 100644 --- a/apps/notifications/models/notification.py +++ b/apps/notifications/models/notification.py @@ -12,6 +12,7 @@ class UserMsgSubscription(JMSBaseModel): verbose_name=_('User') ) receive_backends = models.JSONField(default=list, verbose_name=_('receive backend')) + comment = '' class Meta: verbose_name = _('User message') @@ -25,6 +26,7 @@ class SystemMsgSubscription(JMSBaseModel): users = models.ManyToManyField('users.User', related_name='system_msg_subscriptions') groups = models.ManyToManyField('users.UserGroup', related_name='system_msg_subscriptions') receive_backends = models.JSONField(default=list) + comment = '' message_type_label = '' diff --git a/apps/notifications/models/site_msg.py b/apps/notifications/models/site_msg.py index e08cd5c71..170f21a3f 100644 --- a/apps/notifications/models/site_msg.py +++ b/apps/notifications/models/site_msg.py @@ -6,10 +6,13 @@ __all__ = ('SiteMessageUsers', 'SiteMessage') class SiteMessageUsers(JMSBaseModel): - sitemessage = models.ForeignKey('notifications.SiteMessage', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') - user = models.ForeignKey('users.User', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') + sitemessage = models.ForeignKey('notifications.SiteMessage', on_delete=models.CASCADE, db_constraint=False, + related_name='m2m_sitemessageusers') + user = models.ForeignKey('users.User', on_delete=models.CASCADE, db_constraint=False, + related_name='m2m_sitemessageusers') has_read = models.BooleanField(default=False) read_at = models.DateTimeField(default=None, null=True) + comment = '' class SiteMessage(JMSBaseModel): @@ -24,6 +27,7 @@ class SiteMessage(JMSBaseModel): 'users.User', db_constraint=False, on_delete=models.DO_NOTHING, null=True, default=None, related_name='send_site_message' ) + comment = '' has_read = False read_at = None diff --git a/apps/notifications/signal_handlers.py b/apps/notifications/signal_handlers.py index aaf3480bf..c0b1f1c1c 100644 --- a/apps/notifications/signal_handlers.py +++ b/apps/notifications/signal_handlers.py @@ -1,32 +1,26 @@ -import json -from importlib import import_module import inspect +from importlib import import_module -from django.utils.functional import LazyObject -from django.db.models.signals import post_save -from django.db.models.signals import post_migrate -from django.dispatch import receiver from django.apps import AppConfig +from django.db.models.signals import post_migrate +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.functional import LazyObject +from common.decorator import on_transaction_commit +from common.utils import get_logger +from common.utils.connection import RedisPubSub from notifications.backends import BACKEND from users.models import User -from common.utils.connection import RedisPubSub -from common.utils import get_logger -from common.decorator import on_transaction_commit from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription from .notifications import SystemMessage - logger = get_logger(__name__) -def new_site_msg_pub_sub(): - return RedisPubSub('notifications.SiteMessageCome') - - class NewSiteMsgSubPub(LazyObject): def _setup(self): - self._wrapped = new_site_msg_pub_sub() + self._wrapped = RedisPubSub('notifications.SiteMessageCome') new_site_msg_chan = NewSiteMsgSubPub() @@ -78,7 +72,8 @@ def create_system_messages(app_config: AppConfig, **kwargs): sub, created = SystemMsgSubscription.objects.get_or_create(message_type=message_type) if created: obj.post_insert_to_db(sub) - logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}') + logger.info( + f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}') except ModuleNotFoundError: pass diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 7d2b1f39d..4bcb9be60 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -8,6 +8,7 @@ class DefaultCallback: 'failed': 'failed', 'running': 'running', 'pending': 'pending', + 'timeout': 'timeout', 'unknown': 'unknown' } diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 1258742e0..6ac8cdb83 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -20,6 +20,7 @@ class JMSInventory: self.account_prefer = account_prefer self.account_policy = account_policy self.host_callback = host_callback + self.exclude_hosts = {} @staticmethod def clean_assets(assets): @@ -112,6 +113,7 @@ class JMSInventory: 'secret': account.secret, 'secret_type': account.secret_type } if account else None } + if host['jms_account'] and asset.platform.type == 'oracle': host['jms_account']['mode'] = 'sysdba' if account.privileged else None @@ -194,7 +196,7 @@ class JMSInventory: print(_("Skip hosts below:")) for i, host in enumerate(exclude_hosts, start=1): print("{}: [{}] \t{}".format(i, host['name'], host['error'])) - + self.exclude_hosts[host['name']] = host['error'] hosts = list(filter(lambda x: not x.get('error'), hosts)) data = {'all': {'hosts': {}}} for host in hosts: diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 13d56bd00..280f8f05c 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -13,7 +13,8 @@ class AdHocRunner: "reboot", 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' ] - def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars={}): + def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars={}, + dry_run=False, timeout=-1): self.id = uuid.uuid4() self.inventory = inventory self.pattern = pattern @@ -23,6 +24,8 @@ class AdHocRunner: self.cb = DefaultCallback() self.runner = None self.extra_vars = extra_vars + self.dry_run = dry_run + self.timeout = timeout def check_module(self): if self.module not in self.cmd_modules_choices: @@ -39,6 +42,7 @@ class AdHocRunner: os.mkdir(self.project_dir, 0o755) ansible_runner.run( + timeout=self.timeout if self.timeout > 0 else None, extravars=self.extra_vars, host_pattern=self.pattern, private_data_dir=self.project_dir, diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 2713d448f..aa7b890c5 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -# - -from rest_framework_bulk import BulkModelViewSet - -from common.mixins import CommonApiMixin +from orgs.mixins.api import OrgBulkModelViewSet from ..models import AdHoc from ..serializers import ( AdHocSerializer @@ -14,9 +10,13 @@ __all__ = [ ] -class AdHocViewSet(CommonApiMixin, BulkModelViewSet): +class AdHocViewSet(OrgBulkModelViewSet): serializer_class = AdHocSerializer permission_classes = () + model = AdHoc + def allow_bulk_destroy(self, qs, filtered): + return True def get_queryset(self): - return AdHoc.objects.filter(creator=self.request.user) + queryset = super().get_queryset() + return queryset.filter(creator=self.request.user) diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index a7ba44f22..817e5040e 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -1,13 +1,19 @@ -from rest_framework import viewsets -from rest_framework_bulk import BulkModelViewSet +from django.db.models import Count +from rest_framework.views import APIView +from django.shortcuts import get_object_or_404 +from rest_framework.response import Response -from common.mixins import CommonApiMixin from ops.models import Job, JobExecution from ops.serializers.job import JobSerializer, JobExecutionSerializer -__all__ = ['JobViewSet', 'JobExecutionViewSet'] +__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', + 'JobAssetDetail', 'JobExecutionTaskDetail', 'FrequentUsernames'] from ops.tasks import run_ops_job_execution +from ops.variables import JMS_JOB_VARIABLE_HELP +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import tmp_to_org, get_current_org_id, get_current_org +from assets.models import Account def set_task_to_serializer_data(serializer, task): @@ -16,49 +22,105 @@ def set_task_to_serializer_data(serializer, task): setattr(serializer, "_data", data) -class JobViewSet(CommonApiMixin, BulkModelViewSet): +class JobViewSet(OrgBulkModelViewSet): serializer_class = JobSerializer permission_classes = () + model = Job + + def allow_bulk_destroy(self, qs, filtered): + return True def get_queryset(self): - query_set = Job.objects.filter(creator=self.request.user) + queryset = super().get_queryset() + queryset = queryset.filter(creator=self.request.user) if self.action != 'retrieve': - return query_set.filter(instant=False) - return query_set + return queryset.filter(instant=False) + return queryset def perform_create(self, serializer): + run_after_save = serializer.validated_data.pop('run_after_save', False) instance = serializer.save() - run_after_save = serializer.validated_data.get('run_after_save', False) if instance.instant or run_after_save: self.run_job(instance, serializer) def perform_update(self, serializer): + run_after_save = serializer.validated_data.pop('run_after_save', False) instance = serializer.save() - run_after_save = serializer.validated_data.get('run_after_save', False) if run_after_save: self.run_job(instance, serializer) - @staticmethod - def run_job(job, serializer): + def run_job(self, job, serializer): execution = job.create_execution() + execution.creator = self.request.user + execution.save() task = run_ops_job_execution.delay(execution.id) set_task_to_serializer_data(serializer, task) -class JobExecutionViewSet(CommonApiMixin, BulkModelViewSet): +class JobExecutionViewSet(OrgBulkModelViewSet): serializer_class = JobExecutionSerializer http_method_names = ('get', 'post', 'head', 'options',) permission_classes = () + model = JobExecution def perform_create(self, serializer): instance = serializer.save() + instance.job_version = instance.job.version + instance.creator = self.request.user + instance.save() task = run_ops_job_execution.delay(instance.id) set_task_to_serializer_data(serializer, task) def get_queryset(self): - query_set = JobExecution.objects.filter(creator=self.request.user) - query_set = query_set.filter(creator=self.request.user) + queryset = super().get_queryset() + queryset = queryset.filter(creator=self.request.user) job_id = self.request.query_params.get('job_id') if job_id: - query_set = query_set.filter(job_id=job_id) - return query_set + queryset = queryset.filter(job_id=job_id) + return queryset + + +class JobRunVariableHelpAPIView(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + return Response(data=JMS_JOB_VARIABLE_HELP) + + +class JobAssetDetail(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + execution_id = request.query_params.get('execution_id') + if execution_id: + execution = get_object_or_404(JobExecution, id=execution_id) + return Response(data=execution.assent_result_detail) + + +class JobExecutionTaskDetail(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + org = get_current_org() + task_id = request.query_params.get('task_id') + if task_id: + with tmp_to_org(org): + execution = get_object_or_404(JobExecution, task_id=task_id) + return Response(data={ + 'status': execution.status, + 'is_finished': execution.is_finished, + 'is_success': execution.is_success, + 'time_cost': execution.time_cost, + }) + + +class FrequentUsernames(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + top_accounts = Account.objects.all().values('username').annotate(total=Count('username')).order_by('total') + return Response(data=top_accounts) diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index 9ce525c28..aaafacd58 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -2,9 +2,6 @@ import os import zipfile from django.conf import settings -from rest_framework_bulk import BulkModelViewSet - -from common.mixins import CommonApiMixin from orgs.mixins.api import OrgBulkModelViewSet from ..exception import PlaybookNoValidEntry from ..models import Playbook @@ -19,11 +16,19 @@ def unzip_playbook(src, dist): fz.extract(file, dist) -class PlaybookViewSet(CommonApiMixin, BulkModelViewSet): +class PlaybookViewSet(OrgBulkModelViewSet): serializer_class = PlaybookSerializer permission_classes = () model = Playbook + def allow_bulk_destroy(self, qs, filtered): + return True + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(creator=self.request.user) + return queryset + def perform_create(self, serializer): instance = serializer.save() src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name) diff --git a/apps/ops/celery/__init__.py b/apps/ops/celery/__init__.py index cb7bdcb88..6419fee9c 100644 --- a/apps/ops/celery/__init__.py +++ b/apps/ops/celery/__init__.py @@ -2,12 +2,14 @@ import os -from kombu import Exchange, Queue from celery import Celery +from kombu import Exchange, Queue # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jumpserver.settings') from jumpserver import settings +from .heatbeat import * + # from django.conf import settings app = Celery('jumpserver') diff --git a/apps/ops/celery/heatbeat.py b/apps/ops/celery/heatbeat.py new file mode 100644 index 000000000..339a3c60a --- /dev/null +++ b/apps/ops/celery/heatbeat.py @@ -0,0 +1,25 @@ +from pathlib import Path + +from celery.signals import heartbeat_sent, worker_ready, worker_shutdown + + +@heartbeat_sent.connect +def heartbeat(sender, **kwargs): + worker_name = sender.eventer.hostname.split('@')[0] + heartbeat_path = Path('/tmp/worker_heartbeat_{}'.format(worker_name)) + heartbeat_path.touch() + + +@worker_ready.connect +def worker_ready(sender, **kwargs): + worker_name = sender.hostname.split('@')[0] + ready_path = Path('/tmp/worker_ready_{}'.format(worker_name)) + ready_path.touch() + + +@worker_shutdown.connect +def worker_shutdown(sender, **kwargs): + worker_name = sender.hostname.split('@')[0] + for signal in ['ready', 'heartbeat']: + path = Path('/tmp/worker_{}_{}'.format(signal, worker_name)) + path.unlink(missing_ok=True) diff --git a/apps/ops/const.py b/apps/ops/const.py index 2f68efd3b..c383ef3c7 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -27,3 +27,27 @@ DEFAULT_PASSWORD_RULES = { 'length': DEFAULT_PASSWORD_LENGTH, 'symbol_set': string_punctuation } + + +class Types(models.TextChoices): + adhoc = 'adhoc', _('Adhoc') + playbook = 'playbook', _('Playbook') + + +class RunasPolicies(models.TextChoices): + privileged_only = 'privileged_only', _('Privileged Only') + privileged_first = 'privileged_first', _('Privileged First') + skip = 'skip', _('Skip') + + +class Modules(models.TextChoices): + shell = 'shell', _('Shell') + winshell = 'win_shell', _('Powershell') + python = 'python', _('Python') + + +class JobStatus(models.TextChoices): + running = 'running', _('Running') + success = 'success', _('Success') + timeout = 'timeout', _('Timeout') + failed = 'failed', _('Failed') diff --git a/apps/ops/migrations/0026_auto_20221009_2050.py b/apps/ops/migrations/0026_auto_20221009_2050.py index 699246531..b9965c2bd 100644 --- a/apps/ops/migrations/0026_auto_20221009_2050.py +++ b/apps/ops/migrations/0026_auto_20221009_2050.py @@ -1,13 +1,13 @@ # Generated by Django 3.2.14 on 2022-10-09 12:50 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('assets', '0106_auto_20220916_1556'), @@ -23,7 +23,8 @@ class Migration(migrations.Migration): ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), ('is_periodic', models.BooleanField(default=False)), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), @@ -32,7 +33,7 @@ class Migration(migrations.Migration): ('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')), ('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')), ('path', models.FilePathField(max_length=1024, verbose_name='Playbook')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), ], options={ @@ -52,10 +53,11 @@ class Migration(migrations.Migration): ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), ('path', models.FilePathField(verbose_name='Path')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ], options={ 'verbose_name': 'Playbook template', @@ -74,8 +76,11 @@ class Migration(migrations.Migration): ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), ('path', models.FilePathField(max_length=1024, verbose_name='Run dir')), - ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ops.playbook', verbose_name='Task')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ops.playbook', + verbose_name='Task')), ], options={ 'ordering': ['-date_start'], @@ -85,16 +90,19 @@ class Migration(migrations.Migration): migrations.AddField( model_name='playbook', name='last_execution', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbookexecution', verbose_name='Last execution'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + to='ops.playbookexecution', verbose_name='Last execution'), ), migrations.AddField( model_name='playbook', name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Owner'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, verbose_name='Owner'), ), migrations.AddField( model_name='playbook', name='template', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbooktemplate', verbose_name='Template'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbooktemplate', + verbose_name='Template'), ), ] diff --git a/apps/ops/migrations/0029_auto_20221215_1712.py b/apps/ops/migrations/0029_auto_20221215_1712.py new file mode 100644 index 000000000..b7dc3ea6d --- /dev/null +++ b/apps/ops/migrations/0029_auto_20221215_1712.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.14 on 2022-12-15 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0028_auto_20221205_1627'), + ] + + operations = [ + migrations.AddField( + model_name='adhoc', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AddField( + model_name='job', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AddField( + model_name='jobexecution', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AddField( + model_name='playbook', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0030_auto_20221220_1941.py b/apps/ops/migrations/0030_auto_20221220_1941.py new file mode 100644 index 000000000..cebbd8aa1 --- /dev/null +++ b/apps/ops/migrations/0030_auto_20221220_1941.py @@ -0,0 +1,80 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0029_auto_20221215_1712'), + ] + + operations = [ + migrations.CreateModel( + name='JobAuditLog', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('ops.jobexecution',), + ), + migrations.AddField( + model_name='job', + name='version', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='jobexecution', + name='job_version', + field=models.IntegerField(default=0), + ), + migrations.CreateModel( + name='HistoricalJob', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('instant', models.BooleanField(default=False)), + ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), + ('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, null=True, verbose_name='Module')), + ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), + ('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')), + ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', max_length=128, verbose_name='Type')), + ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), + ('runas_policy', models.CharField(choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), + ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), + ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), + ('comment', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('version', models.IntegerField(default=0)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ops.playbook', verbose_name='Playbook')), + ], + options={ + 'verbose_name': 'historical job', + 'verbose_name_plural': 'historical jobs', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/apps/ops/migrations/0031_auto_20221220_1956.py b/apps/ops/migrations/0031_auto_20221220_1956.py new file mode 100644 index 000000000..9cbc21547 --- /dev/null +++ b/apps/ops/migrations/0031_auto_20221220_1956.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('ops', '0030_auto_20221220_1941'), + ] + + operations = [ + migrations.AddField( + model_name='jobexecution', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='adhoc', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='adhoc', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='job', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='job', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='jobexecution', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='jobexecution', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='playbook', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='playbook', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/ops/migrations/0032_auto_20221221_1513.py b/apps/ops/migrations/0032_auto_20221221_1513.py new file mode 100644 index 000000000..3a706f4b7 --- /dev/null +++ b/apps/ops/migrations/0032_auto_20221221_1513.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-12-21 07:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0031_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterField( + model_name='historicaljob', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='historicaljob', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/ops/migrations/0033_auto_20221223_1536.py b/apps/ops/migrations/0033_auto_20221223_1536.py new file mode 100644 index 000000000..f46955209 --- /dev/null +++ b/apps/ops/migrations/0033_auto_20221223_1536.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.16 on 2022-12-23 07:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0032_auto_20221221_1513'), + ] + + operations = [ + migrations.AlterModelOptions( + name='adhoc', + options={'verbose_name': 'AdHoc'}, + ), + migrations.AlterModelOptions( + name='celerytask', + options={'ordering': ('name',), 'verbose_name': 'Celery Task'}, + ), + migrations.AlterModelOptions( + name='celerytaskexecution', + options={'verbose_name': 'Celery Task Execution'}, + ), + migrations.AlterModelOptions( + name='historicaljob', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Job', 'verbose_name_plural': 'historical Jobs'}, + ), + migrations.AlterModelOptions( + name='job', + options={'ordering': ['date_created'], 'verbose_name': 'Job'}, + ), + migrations.AlterModelOptions( + name='jobauditlog', + options={'verbose_name': 'Job audit log'}, + ), + migrations.AlterModelOptions( + name='jobexecution', + options={'ordering': ['-date_created'], 'verbose_name': 'Job Execution'}, + ), + ] diff --git a/apps/ops/migrations/0034_alter_celerytask_options.py b/apps/ops/migrations/0034_alter_celerytask_options.py new file mode 100644 index 000000000..9645782c4 --- /dev/null +++ b/apps/ops/migrations/0034_alter_celerytask_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2022-12-27 06:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0033_auto_20221223_1536'), + ] + + operations = [ + migrations.AlterModelOptions( + name='celerytask', + options={'ordering': ('name',), 'permissions': [('view_taskmonitor', 'Can view task monitor')], 'verbose_name': 'Celery Task'}, + ), + ] diff --git a/apps/ops/migrations/0035_auto_20221227_1520.py b/apps/ops/migrations/0035_auto_20221227_1520.py new file mode 100644 index 000000000..36752a9bb --- /dev/null +++ b/apps/ops/migrations/0035_auto_20221227_1520.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.14 on 2022-12-27 07:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0034_alter_celerytask_options'), + ] + + operations = [ + migrations.AlterField( + model_name='historicaljob', + name='module', + field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), + ), + migrations.AlterField( + model_name='historicaljob', + name='timeout', + field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), + ), + migrations.AlterField( + model_name='job', + name='module', + field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), + ), + migrations.AlterField( + model_name='job', + name='timeout', + field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index d6fc27038..4e8219ee1 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -9,10 +9,12 @@ from common.utils import get_logger __all__ = ["AdHoc"] +from orgs.mixins.models import JMSOrgBaseModel + logger = get_logger(__file__) -class AdHoc(JMSBaseModel): +class AdHoc(JMSOrgBaseModel): class Modules(models.TextChoices): shell = 'shell', _('Shell') winshell = 'win_shell', _('Powershell') @@ -39,3 +41,6 @@ class AdHoc(JMSBaseModel): def __str__(self): return "{}: {}".format(self.module, self.args) + + class Meta: + verbose_name = _("AdHoc") diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py index 7b98dc3ab..316eca876 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -44,7 +44,11 @@ class CeleryTask(models.Model): return "green" class Meta: + verbose_name = _("Celery Task") ordering = ('name',) + permissions = [ + ('view_taskmonitor', _('Can view task monitor')) + ] class CeleryTaskExecution(models.Model): @@ -77,3 +81,6 @@ class CeleryTaskExecution(models.Model): def __str__(self): return "{}: {}".format(self.name, self.id) + + class Meta: + verbose_name = _("Celery Task Execution") diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 211d386d9..85f960df1 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -1,43 +1,33 @@ import json +import logging import os import uuid -import logging +from celery import current_task from django.conf import settings from django.db import models -from django.utils.translation import gettext_lazy as _ from django.utils import timezone -from celery import current_task +from django.utils.translation import gettext_lazy as _ -__all__ = ["Job", "JobExecution"] +__all__ = ["Job", "JobExecution", "JobAuditLog"] + +from simple_history.models import HistoricalRecords -from common.db.models import JMSBaseModel from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner from ops.mixin import PeriodTaskModelMixin +from ops.variables import * +from ops.const import Types, Modules, RunasPolicies, JobStatus +from orgs.mixins.models import JMSOrgBaseModel -class Job(JMSBaseModel, PeriodTaskModelMixin): - class Types(models.TextChoices): - adhoc = 'adhoc', _('Adhoc') - playbook = 'playbook', _('Playbook') - - class RunasPolicies(models.TextChoices): - privileged_only = 'privileged_only', _('Privileged Only') - privileged_first = 'privileged_first', _('Privileged First') - skip = 'skip', _('Skip') - - class Modules(models.TextChoices): - shell = 'shell', _('Shell') - winshell = 'win_shell', _('Powershell') - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class Job(JMSOrgBaseModel, PeriodTaskModelMixin): name = models.CharField(max_length=128, null=True, verbose_name=_('Name')) instant = models.BooleanField(default=False) args = models.CharField(max_length=1024, default='', verbose_name=_('Args'), null=True, blank=True) module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, verbose_name=_('Module'), null=True) chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True) - timeout = models.IntegerField(default=60, verbose_name=_('Timeout (Seconds)')) + timeout = models.IntegerField(default=-1, verbose_name=_('Timeout (Seconds)')) playbook = models.ForeignKey('ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.SET_NULL) type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Type")) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) @@ -48,6 +38,11 @@ class Job(JMSBaseModel, PeriodTaskModelMixin): use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define'))) parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define')) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + version = models.IntegerField(default=0) + history = HistoricalRecords() + + def get_history(self, version): + return self.history.filter(version=version).first() @property def last_execution(self): @@ -90,17 +85,19 @@ class Job(JMSBaseModel, PeriodTaskModelMixin): return JMSInventory(self.assets.all(), self.runas_policy, self.runas) def create_execution(self): - return self.executions.create() + return self.executions.create(job_version=self.version) class Meta: + verbose_name = _("Job") ordering = ['date_created'] -class JobExecution(JMSBaseModel): +class JobExecution(JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) task_id = models.UUIDField(null=True) - status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') + status = models.CharField(max_length=16, verbose_name=_('Status'), default=JobStatus.running) job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True) + job_version = models.IntegerField(default=0) parameters = models.JSONField(default=dict, verbose_name=_('Parameters')) result = models.JSONField(blank=True, null=True, verbose_name=_('Result')) summary = models.JSONField(default=dict, verbose_name=_('Summary')) @@ -109,39 +106,120 @@ class JobExecution(JMSBaseModel): date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + @property + def current_job(self): + if self.job.version != self.job_version: + return self.job.get_history(self.job_version) + return self.job + + @property + def material(self): + if self.current_job.type == 'adhoc': + return "{}:{}".format(self.current_job.module, self.current_job.args) + if self.current_job.type == 'playbook': + return "{}:{}:{}".format(self.org.name, self.current_job.creator.name, self.current_job.playbook.name) + + @property + def assent_result_detail(self): + if self.is_finished and not self.summary.get('error', None): + result = { + "summary": self.count, + "detail": [], + } + for asset in self.current_job.assets.all(): + asset_detail = { + "name": asset.name, + "status": "ok", + "tasks": [], + } + if self.summary.get("excludes", None) and self.summary["excludes"].get(asset.name, None): + asset_detail.update({"status": "excludes"}) + result["detail"].append(asset_detail) + break + if self.result["dark"].get(asset.name, None): + asset_detail.update({"status": "failed"}) + for key, task in self.result["dark"][asset.name].items(): + task_detail = {"name": key, + "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))} + asset_detail["tasks"].append(task_detail) + if self.result["failures"].get(asset.name, None): + asset_detail.update({"status": "failed"}) + for key, task in self.result["failures"][asset.name].items(): + task_detail = {"name": key, + "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))} + asset_detail["tasks"].append(task_detail) + + if self.result["ok"].get(asset.name, None): + for key, task in self.result["ok"][asset.name].items(): + task_detail = {"name": key, + "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))} + asset_detail["tasks"].append(task_detail) + result["detail"].append(asset_detail) + return result + @property def job_type(self): - return self.job.type + return self.current_job.type def compile_shell(self): - if self.job.type != 'adhoc': + if self.current_job.type != 'adhoc': return - result = "{}{}{} ".format('\'', self.job.args, '\'') - result += "chdir={}".format(self.job.chdir) + result = self.current_job.args + if self.current_job.chdir: + result += " chdir={}".format(self.current_job.chdir) + if self.current_job.module in ['python']: + result += " executable={}".format(self.current_job.module) return result def get_runner(self): - inv = self.job.inventory + inv = self.current_job.inventory inv.write_to_file(self.inventory_path) + self.summary = self.result = {"excludes": {}} + if len(inv.exclude_hosts) > 0: + self.summary.update({"excludes": inv.exclude_hosts}) + self.result.update({"excludes": inv.exclude_hosts}) + self.save() + if isinstance(self.parameters, str): extra_vars = json.loads(self.parameters) else: extra_vars = {} - if self.job.type == 'adhoc': + static_variables = self.gather_static_variables() + extra_vars.update(static_variables) + + if self.current_job.type == 'adhoc': args = self.compile_shell() + module = "shell" + if self.current_job.module not in ['python']: + module = self.current_job.module + runner = AdHocRunner( - self.inventory_path, self.job.module, module_args=args, - pattern="all", project_dir=self.private_dir, extra_vars=extra_vars, + self.inventory_path, + module, + timeout=self.current_job.timeout, + module_args=args, + pattern="all", + project_dir=self.private_dir, + extra_vars=extra_vars, ) - elif self.job.type == 'playbook': + elif self.current_job.type == 'playbook': runner = PlaybookRunner( - self.inventory_path, self.job.playbook.entry + self.inventory_path, self.current_job.playbook.entry ) else: raise Exception("unsupported job type") return runner + def gather_static_variables(self): + default = { + JMS_JOB_ID: str(self.current_job.id), + JMS_JOB_NAME: self.current_job.name, + } + if self.creator: + default.update({JMS_USERNAME: self.creator.username}) + return default + @property def short_id(self): return str(self.id).split('-')[-1] @@ -160,11 +238,11 @@ class JobExecution(JMSBaseModel): @property def is_finished(self): - return self.status in ['success', 'failed'] + return self.status in [JobStatus.success, JobStatus.failed, JobStatus.timeout] @property def is_success(self): - return self.status == 'success' + return self.status == JobStatus.success @property def inventory_path(self): @@ -173,23 +251,26 @@ class JobExecution(JMSBaseModel): @property def private_dir(self): uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id - job_name = self.job.name if self.job.name else 'instant' + job_name = self.current_job.name if self.current_job.name else 'instant' return os.path.join(settings.ANSIBLE_DIR, job_name, uniq) def set_error(self, error): this = self.__class__.objects.get(id=self.id) # 重新获取一次,避免数据库超时连接超时 - this.status = 'failed' - this.summary['error'] = str(error) + this.status = JobStatus.failed + this.summary.update({'error': str(error)}) this.finish_task() def set_result(self, cb): status_mapper = { - 'successful': 'success', + 'successful': JobStatus.success, } this = self.__class__.objects.get(id=self.id) this.status = status_mapper.get(cb.status, cb.status) - this.summary = cb.summary - this.result = cb.result + this.summary.update(cb.summary) + if this.result: + this.result.update(cb.result) + else: + this.result = cb.result this.finish_task() def finish_task(self): @@ -216,4 +297,15 @@ class JobExecution(JMSBaseModel): self.set_error(e) class Meta: + verbose_name = _("Job Execution") ordering = ['-date_created'] + + +class JobAuditLog(JobExecution): + @property + def creator_name(self): + return self.creator.name + + class Meta: + proxy = True + verbose_name = _("Job audit log") diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index 59688f76d..f92968762 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -5,11 +5,11 @@ from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ -from common.db.models import JMSBaseModel from ops.exception import PlaybookNoValidEntry +from orgs.mixins.models import JMSOrgBaseModel -class Playbook(JMSBaseModel): +class Playbook(JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name'), null=True) path = models.FileField(upload_to='playbooks/') diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 9883e104e..7db49fdcd 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -4,10 +4,11 @@ from __future__ import unicode_literals from rest_framework import serializers from common.drf.fields import ReadableHiddenField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AdHoc -class AdHocSerializer(serializers.ModelSerializer): +class AdHocSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) row_count = serializers.IntegerField(read_only=True) size = serializers.IntegerField(read_only=True) diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 05993dc08..4b6ea82d1 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -1,18 +1,31 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers + +from assets.models import Node from common.drf.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution +from ops.models.job import JobAuditLog +from orgs.mixins.serializers import BulkOrgResourceModelSerializer -class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin): +class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) - run_after_save = serializers.BooleanField(label=_("Run after save"), read_only=True, default=False, required=False) + run_after_save = serializers.BooleanField(label=_("Run after save"), default=False, required=False) + nodes = serializers.ListField(required=False, child=serializers.CharField()) + + def create(self, validated_data): + assets = validated_data.__getitem__('assets') + node_ids = validated_data.pop('nodes') + if node_ids: + nodes = Node.objects.filter(id__in=node_ids) + assets.extend( + Node.get_nodes_all_assets(*nodes).exclude(id__in=[asset.id for asset in assets])) + return super().create(validated_data) class Meta: model = Job - read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost", - "run_after_save"] + read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"] fields = read_only_fields + [ "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "creator", "use_parameter_define", @@ -21,17 +34,21 @@ class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin): "chdir", "comment", "summary", - "is_periodic", "interval", "crontab" + "is_periodic", "interval", "crontab", "run_after_save", "nodes" ] -class JobExecutionSerializer(serializers.ModelSerializer): +class JobExecutionSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) + job_type = serializers.ReadOnlyField(label=_("Job type")) + material = serializers.ReadOnlyField(label=_("Material")) class Meta: model = JobExecution - read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created', - 'is_success', 'task_id', 'short_id', 'job_type', 'creator'] + read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', + 'date_finished', + 'date_created', + 'is_success', 'task_id', 'short_id', 'job_type', 'summary', 'material'] fields = read_only_fields + [ - "job", "parameters" + "job", "parameters", "creator" ] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index 0334bdd45..bcfd75acd 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -12,7 +12,7 @@ def parse_playbook_name(path): return file_name.split(".")[-2] -class PlaybookSerializer(serializers.ModelSerializer): +class PlaybookSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) path = serializers.FileField(required=False) diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index 965bd494c..29add5bcb 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -1,18 +1,17 @@ import ast -from celery import signals -from django.db import transaction +from celery import signals from django.core.cache import cache -from django.dispatch import receiver +from django.db import transaction +from django.db.models.signals import pre_save from django.db.utils import ProgrammingError +from django.dispatch import receiver from django.utils import translation, timezone from django.utils.translation import gettext as _ -from common.signals import django_ready from common.db.utils import close_old_connections, get_logger - from .celery import app -from .models import CeleryTaskExecution, CeleryTask +from .models import CeleryTaskExecution, CeleryTask, Job logger = get_logger(__name__) @@ -20,8 +19,18 @@ TASK_LANG_CACHE_KEY = 'TASK_LANG_{}' TASK_LANG_CACHE_TTL = 1800 -@receiver(django_ready) +@receiver(pre_save, sender=Job) +def on_account_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version += 1 + + +@receiver(signals.worker_ready) def sync_registered_tasks(*args, **kwargs): + synced = cache.get('synced_registered_tasks', False) + if synced: + return + cache.set('synced_registered_tasks', True, 60) with transaction.atomic(): try: db_tasks = CeleryTask.objects.all() diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index bd4d5d448..19eb488e7 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -28,23 +28,22 @@ logger = get_logger(__file__) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task")) def run_ops_job(job_id): job = get_object_or_none(Job, id=job_id) - with tmp_to_org(job.org): - execution = job.create_execution() - run_ops_job_execution(execution) + execution = job.create_execution() + run_ops_job_execution(execution) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) def run_ops_job_execution(execution_id, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) - with tmp_to_org(execution.org): - try: + try: + with tmp_to_org(execution.org): execution.start() - except SoftTimeLimitExceeded: - execution.set_error('Run timeout') - logger.error("Run adhoc timeout") - except Exception as e: - execution.set_error(e) - logger.error("Start adhoc execution error: {}".format(e)) + except SoftTimeLimitExceeded: + execution.set_error('Run timeout') + logger.error("Run adhoc timeout") + except Exception as e: + execution.set_error(e) + logger.error("Start adhoc execution error: {}".format(e)) @shared_task(verbose_name=_('Periodic clear celery tasks')) diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index a8b71734f..5d859b801 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -23,7 +23,10 @@ router.register(r'tasks', api.CeleryTaskViewSet, 'task') router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-executions') urlpatterns = [ - + path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), + path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), + path('job-execution/task-detail/', api.JobExecutionTaskDetail.as_view(), name='task-detail'), + path('frequent-username/', api.FrequentUsernames.as_view(), name='frequent-usernames'), path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), path('celery/task//task-execution//log/', api.CeleryTaskExecutionLogApi.as_view(), diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 456e73d00..e8e6cadca 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -4,7 +4,7 @@ import uuid from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, get_object_or_none +from common.utils import get_logger, get_object_or_none, make_dirs from orgs.utils import org_aware_func from jumpserver.const import PROJECT_DIR diff --git a/apps/ops/variables.py b/apps/ops/variables.py new file mode 100644 index 000000000..2d9c078af --- /dev/null +++ b/apps/ops/variables.py @@ -0,0 +1,33 @@ +from django.utils.translation import gettext_lazy as _ + +# JumpServer +JMS_USERNAME = "jms_username" + +# ASSENT +JMS_ASSET_ID = "jms_asset.id" +JMS_ASSET_TYPE = "jms_asset.type" +JMS_ASSET_CATEGORY = "jms_asset.category" +JMS_ASSET_PROTOCOL = "jms_asset.protocol" +JMS_ASSET_PORT = "jms_asset.port" +JMS_ASSET_NAME = "jms_asset.name" +JMS_ASSET_ADDRESS = "jms_asset.address" + +# Account +JMS_ACCOUNT_ID = "jms_account.id" +JMS_ACCOUNT_USERNAME = "jms_account.name" + +# JOB +JMS_JOB_ID = "jms_job_id" +JMS_JOB_NAME = "jms_job_name" + +JMS_JOB_VARIABLE_HELP = { + JMS_USERNAME: _('The current user`s username of JumpServer'), + JMS_ASSET_ID: _('The id of the asset in the JumpServer'), + JMS_ASSET_TYPE: _('The type of the asset in the JumpServer'), + JMS_ASSET_CATEGORY: _('The category of the asset in the JumpServer'), + JMS_ASSET_NAME: _('The name of the asset in the JumpServer'), + JMS_ASSET_ADDRESS: _('Address used to connect this asset in JumpServer'), + JMS_ASSET_PORT: _('Port used to connect this asset in JumpServer'), + JMS_JOB_ID: _('ID of the job'), + JMS_JOB_NAME: _('Name of the job'), +} diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 24023a33e..5c261f76f 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -67,11 +67,12 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer): task_end_mark.append(1) elif len(task_end_mark) == 2: logger.debug('Task log end: {}'.format(task_id)) + await self.send_json({'event': 'end', 'task': task_id}) break await asyncio.sleep(0.2) except OSError as e: logger.warn('Task log path open failed: {}'.format(e)) - await self.close() + # await self.close() async def disconnect(self, close_code): self.disconnected = True diff --git a/apps/orgs/api.py b/apps/orgs/api.py index da9b1530f..e18ddf55e 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -15,7 +15,7 @@ from .serializers import ( from users.models import User, UserGroup from assets.models import ( Asset, Domain, Label, Node, - CommandFilter, CommandFilterRule, GatheredUser + CommandFilter, CommandFilterRule ) from perms.models import AssetPermission from orgs.utils import current_org, tmp_to_root_org @@ -28,8 +28,7 @@ logger = get_logger(__file__) # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ User, UserGroup, Asset, Label, Domain, Node, Label, - CommandFilter, CommandFilterRule, GatheredUser, - AssetPermission, + CommandFilter, CommandFilterRule, AssetPermission, ] diff --git a/apps/orgs/migrations/0001_initial.py b/apps/orgs/migrations/0001_initial.py index 7241ce6a2..a4705e719 100644 --- a/apps/orgs/migrations/0001_initial.py +++ b/apps/orgs/migrations/0001_initial.py @@ -1,12 +1,12 @@ # Generated by Django 2.0.7 on 2018-08-07 03:16 +import uuid + from django.conf import settings from django.db import migrations, models -import uuid class Migration(migrations.Migration): - initial = True dependencies = [ @@ -21,7 +21,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('admins', models.ManyToManyField(blank=True, related_name='admin_orgs', to=settings.AUTH_USER_MODEL)), ('users', models.ManyToManyField(blank=True, related_name='orgs', to=settings.AUTH_USER_MODEL)), ], diff --git a/apps/orgs/migrations/0013_alter_organization_options.py b/apps/orgs/migrations/0013_alter_organization_options.py index 6dfd004da..e868a87a3 100644 --- a/apps/orgs/migrations/0013_alter_organization_options.py +++ b/apps/orgs/migrations/0013_alter_organization_options.py @@ -14,7 +14,4 @@ class Migration(migrations.Migration): name='organization', options={'permissions': (('view_rootorg', 'Can view root org'), ('view_alljoinedorg', 'Can view all joined org')), 'verbose_name': 'Organization'}, ), - migrations.DeleteModel( - name='OrganizationMember', - ), ] diff --git a/apps/orgs/migrations/0014_organization_builtin.py b/apps/orgs/migrations/0014_organization_builtin.py index 6541fe1a7..a9b407ffc 100644 --- a/apps/orgs/migrations/0014_organization_builtin.py +++ b/apps/orgs/migrations/0014_organization_builtin.py @@ -27,4 +27,7 @@ class Migration(migrations.Migration): field=models.BooleanField(default=False, verbose_name='Builtin'), ), migrations.RunPython(update_builtin_org), + migrations.DeleteModel( + name='OrganizationMember', + ), ] diff --git a/apps/orgs/migrations/0015_auto_20221220_1956.py b/apps/orgs/migrations/0015_auto_20221220_1956.py new file mode 100644 index 000000000..df92f6862 --- /dev/null +++ b/apps/orgs/migrations/0015_auto_20221220_1956.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0014_organization_builtin'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='organization', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='organization', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 0795edc2e..d9eef1e2f 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- # +from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ValidationError -from common.utils import get_logger from common.db.models import JMSBaseModel +from common.utils import get_logger, lazyproperty +from ..models import Organization from ..utils import ( set_current_org, get_current_org, current_org, filter_org_queryset ) -from ..models import Organization logger = get_logger(__file__) @@ -75,7 +75,7 @@ class OrgModelMixin(models.Model): self.org_id = org.id return super().save(*args, **kwargs) - @property + @lazyproperty def org(self): return Organization.get_instance(self.org_id) diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 73dc3c6ad..be88ec242 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -1,10 +1,9 @@ -import uuid - from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.utils import lazyproperty, settings +from common.db.models import JMSBaseModel from common.tree import TreeNode +from common.utils import lazyproperty, settings class OrgRoleMixin: @@ -33,7 +32,6 @@ class OrgRoleMixin: def get_origin_role_members(self, role_name): from rbac.models import OrgRoleBinding - from users.models import User from rbac.builtin import BuiltinRole from .utils import tmp_to_org @@ -65,13 +63,9 @@ class OrgRoleMixin: return self.get_origin_role_members('user') -class Organization(OrgRoleMixin, models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class Organization(OrgRoleMixin, JMSBaseModel): name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) - created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) builtin = models.BooleanField(default=False, verbose_name=_('Builtin')) - date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) members = models.ManyToManyField( 'users.User', related_name='orgs', through='rbac.RoleBinding', through_fields=('org', 'user') ) @@ -132,6 +126,7 @@ class Organization(OrgRoleMixin, models.Model): @classmethod def expire_orgs_mapping(cls): + print("Expire orgs mapping: ") cls.orgs_mapping = None def org_id(self): diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index b32cba2cd..802e9c299 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -3,35 +3,30 @@ from collections import defaultdict from functools import partial -import django.db.utils -from django.dispatch import receiver from django.conf import settings -from django.db.utils import ProgrammingError, OperationalError -from django.utils.functional import LazyObject from django.db.models.signals import post_save, pre_delete, m2m_changed +from django.db.utils import ProgrammingError, OperationalError +from django.dispatch import receiver +from django.utils.functional import LazyObject -from orgs.utils import tmp_to_org, set_to_default_org -from orgs.models import Organization -from orgs.hands import set_current_org, Node, get_current_org -from perms.models import AssetPermission -from users.models import UserGroup, User from common.const.signals import PRE_REMOVE, POST_REMOVE from common.decorator import on_transaction_commit from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub +from orgs.hands import set_current_org, Node, get_current_org +from orgs.models import Organization +from orgs.utils import tmp_to_org, set_to_default_org +from perms.models import AssetPermission +from users.models import UserGroup, User from users.signals import post_user_leave_org logger = get_logger(__file__) -def get_orgs_mapping_for_memory_pub_sub(): - return RedisPubSub('fm.orgs_mapping') - - class OrgsMappingForMemoryPubSub(LazyObject): def _setup(self): - self._wrapped = get_orgs_mapping_for_memory_pub_sub() + self._wrapped = RedisPubSub('fm.orgs_mapping') orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub() @@ -61,6 +56,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs): def on_org_create_or_update(sender, instance, created=False, **kwargs): # 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到 expire_orgs_mapping_for_memory(instance.id) + old_org = get_current_org() set_current_org(instance) node_root = Node.org_root() diff --git a/apps/perms/api/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py index c2c116248..5b85cb971 100644 --- a/apps/perms/api/asset_permission_relation.py +++ b/apps/perms/api/asset_permission_relation.py @@ -9,7 +9,7 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org from perms import serializers from perms import models -from perms.utils.user_permission import UserGrantedAssetsQueryUtils +from perms.utils import AssetPermissionPermAssetUtil from assets.serializers import AccountSerializer __all__ = [ @@ -95,8 +95,7 @@ class AssetPermissionAllAssetListApi(generics.ListAPIView): def get_queryset(self): pk = self.kwargs.get("pk") - query_utils = UserGrantedAssetsQueryUtils(None, asset_perm_ids=[pk]) - assets = query_utils.get_all_granted_assets() + assets = AssetPermissionPermAssetUtil(perm_ids=[pk]).get_all_assets() return assets diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index 48f94f6f7..850d1385b 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -6,14 +6,10 @@ from django.db.models import Q from rest_framework.generics import ListAPIView from rest_framework.response import Response -from common.utils import lazyproperty -from perms.models import AssetPermission -from assets.models import Asset, Node -from . import user_permission as uapi -from perms import serializers -from perms.utils import PermAccountUtil from assets.api.mixin import SerializeToTreeNodeMixin -from users.models import UserGroup +from assets.models import Asset, Node +from perms import serializers +from perms.models import AssetPermission __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', @@ -23,8 +19,8 @@ __all__ = [ class UserGroupGrantedAssetsApi(ListAPIView): - serializer_class = serializers.AssetGrantedSerializer - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields + serializer_class = serializers.AssetPermedSerializer + only_fields = serializers.AssetPermedSerializer.Meta.only_fields filterset_fields = ['name', 'address', 'id', 'comment'] search_fields = ['name', 'address', 'comment'] rbac_perms = { @@ -60,8 +56,8 @@ class UserGroupGrantedAssetsApi(ListAPIView): class UserGroupGrantedNodeAssetsApi(ListAPIView): - serializer_class = serializers.AssetGrantedSerializer - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields + serializer_class = serializers.AssetPermedSerializer + only_fields = serializers.AssetPermedSerializer.Meta.only_fields filterset_fields = ['name', 'address', 'id', 'comment'] search_fields = ['name', 'address', 'comment'] rbac_perms = { @@ -101,11 +97,11 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): granted_node_q |= Q(nodes__key=_key) granted_asset_q = ( - Q(granted_by_permissions__id__in=asset_perm_ids) & - ( - Q(nodes__key__startswith=f'{node.key}:') | - Q(nodes__key=node.key) - ) + Q(granted_by_permissions__id__in=asset_perm_ids) & + ( + Q(nodes__key__startswith=f'{node.key}:') | + Q(nodes__key=node.key) + ) ) assets = Asset.objects.filter( @@ -115,7 +111,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): class UserGroupGrantedNodesApi(ListAPIView): - serializer_class = serializers.NodeGrantedSerializer + serializer_class = serializers.NodePermedSerializer rbac_perms = { 'list': 'perms.view_usergroupassets', } diff --git a/apps/perms/api/user_permission/__init__.py b/apps/perms/api/user_permission/__init__.py index 55bc108b4..dc67ce7f2 100644 --- a/apps/perms/api/user_permission/__init__.py +++ b/apps/perms/api/user_permission/__init__.py @@ -2,5 +2,5 @@ # from .nodes import * from .assets import * -from .nodes_with_assets import * from .accounts import * +from .tree import * diff --git a/apps/perms/api/user_permission/accounts.py b/apps/perms/api/user_permission/accounts.py index 96c09667f..bb94d12aa 100644 --- a/apps/perms/api/user_permission/accounts.py +++ b/apps/perms/api/user_permission/accounts.py @@ -25,6 +25,5 @@ class UserPermedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView): return asset def get_queryset(self): - util = PermAccountUtil() - accounts = util.get_permed_accounts_for_user(self.user, self.asset) + accounts = PermAccountUtil().get_permed_accounts_for_user(self.user, self.asset) return accounts diff --git a/apps/perms/api/user_permission/assets.py b/apps/perms/api/user_permission/assets.py index d3ee274aa..e499a3127 100644 --- a/apps/perms/api/user_permission/assets.py +++ b/apps/perms/api/user_permission/assets.py @@ -1,109 +1,78 @@ -from django.conf import settings +import abc from rest_framework.generics import ListAPIView from assets.models import Asset, Node -from common.utils import get_logger +from assets.api.asset.asset import AssetFilterSet from perms import serializers -from perms.pagination import AllGrantedAssetPagination -from perms.pagination import NodeGrantedAssetPagination -from perms.utils.user_permission import UserGrantedAssetsQueryUtils +from perms.pagination import AllPermedAssetPagination +from perms.pagination import NodePermedAssetPagination +from perms.utils import UserPermAssetUtil +from common.utils import get_logger, lazyproperty + from .mixin import ( - SelfOrPKUserMixin, RebuildTreeMixin, - PermedAssetSerializerMixin, AssetsTreeFormatMixin + SelfOrPKUserMixin ) + __all__ = [ + 'UserAllPermedAssetsApi', 'UserDirectPermedAssetsApi', 'UserFavoriteAssetsApi', - 'UserDirectPermedAssetsAsTreeApi', - 'UserUngroupAssetsAsTreeApi', - 'UserAllPermedAssetsApi', 'UserPermedNodeAssetsApi', ] logger = get_logger(__name__) -class UserDirectPermedAssetsApi(SelfOrPKUserMixin, PermedAssetSerializerMixin, ListAPIView): - """ 直接授权给用户的资产 """ - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields +class BaseUserPermedAssetsApi(SelfOrPKUserMixin, ListAPIView): + ordering = ('name',) + ordering_fields = ("name", "address") + search_fields = ('name', 'address', 'comment') + filterset_class = AssetFilterSet + serializer_class = serializers.AssetPermedSerializer + only_fields = serializers.AssetPermedSerializer.Meta.only_fields def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Asset.objects.none() - - assets = UserGrantedAssetsQueryUtils(self.user) \ - .get_direct_granted_assets() \ - .prefetch_related('platform') \ - .only(*self.only_fields) - return assets - - -class UserFavoriteAssetsApi(SelfOrPKUserMixin, PermedAssetSerializerMixin, ListAPIView): - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - """ 用户收藏的授权资产 """ - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - - user = self.user - utils = UserGrantedAssetsQueryUtils(user) - assets = utils.get_favorite_assets() + assets = self.get_assets() assets = assets.prefetch_related('platform').only(*self.only_fields) return assets + @abc.abstractmethod + def get_assets(self): + return Asset.objects.none() -class UserDirectPermedAssetsAsTreeApi(RebuildTreeMixin, AssetsTreeFormatMixin, UserDirectPermedAssetsApi): - """ 用户直接授权的资产作为树 """ - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields + query_asset_util: UserPermAssetUtil - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - - assets = UserGrantedAssetsQueryUtils(self.user) \ - .get_direct_granted_assets() \ - .prefetch_related('platform') \ - .only(*self.only_fields) - return assets + @lazyproperty + def query_asset_util(self): + return UserPermAssetUtil(self.user) -class UserUngroupAssetsAsTreeApi(UserDirectPermedAssetsAsTreeApi): - """ 用户未分组节点下的资产作为树 """ +class UserAllPermedAssetsApi(BaseUserPermedAssetsApi): + pagination_class = AllPermedAssetPagination - def get_queryset(self): - queryset = super().get_queryset() - if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - queryset = queryset.none() - return queryset + def get_assets(self): + return self.query_asset_util.get_all_assets() -class UserAllPermedAssetsApi(SelfOrPKUserMixin, PermedAssetSerializerMixin, ListAPIView): - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - pagination_class = AllGrantedAssetPagination - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - queryset = UserGrantedAssetsQueryUtils(self.user).get_all_granted_assets() - only_fields = [i for i in self.only_fields if i not in ['protocols']] - queryset = queryset.prefetch_related('platform', 'protocols').only(*only_fields) - return queryset +class UserDirectPermedAssetsApi(BaseUserPermedAssetsApi): + def get_assets(self): + return self.query_asset_util.get_direct_assets() -class UserPermedNodeAssetsApi(SelfOrPKUserMixin, PermedAssetSerializerMixin, ListAPIView): - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - pagination_class = NodeGrantedAssetPagination - kwargs: dict +class UserFavoriteAssetsApi(BaseUserPermedAssetsApi): + def get_assets(self): + return self.query_asset_util.get_favorite_assets() + + +class UserPermedNodeAssetsApi(BaseUserPermedAssetsApi): + pagination_class = NodePermedAssetPagination pagination_node: Node - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() + def get_assets(self): node_id = self.kwargs.get("node_id") - - node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(node_id) - assets = assets.prefetch_related('platform').only(*self.only_fields) + node, assets = self.query_asset_util.get_node_all_assets(node_id) self.pagination_node = node return assets diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 7be96caed..4f4d48181 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -3,27 +3,13 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from rest_framework.request import Request -from rest_framework.response import Response -from assets.api.asset.asset import AssetFilterSet -from assets.api.mixin import SerializeToTreeNodeMixin -from common.exceptions import JMSObjectDoesNotExist -from common.http import is_true -from common.utils import is_uuid -from perms import serializers -from perms.utils.user_permission import UserGrantedTreeRefreshController -from rbac.permissions import RBACPermission from users.models import User +from rbac.permissions import RBACPermission +from common.utils import is_uuid +from common.exceptions import JMSObjectDoesNotExist - -class RebuildTreeMixin: - user: User - - def get(self, request: Request, *args, **kwargs): - force = is_true(request.query_params.get('rebuild_tree')) - controller = UserGrantedTreeRefreshController(self.user) - controller.refresh_if_need(force) - return super().get(request, *args, **kwargs) +__all__ = ['SelfOrPKUserMixin'] class SelfOrPKUserMixin: @@ -72,32 +58,3 @@ class SelfOrPKUserMixin: def request_user_is_self(self): return self.kwargs.get('user') in ['my', 'self'] - -class PermedAssetSerializerMixin: - serializer_class = serializers.AssetGrantedSerializer - filterset_class = AssetFilterSet - search_fields = ['name', 'address', 'comment'] - ordering_fields = ("name", "address") - ordering = ('name',) - - -class AssetsTreeFormatMixin(SerializeToTreeNodeMixin): - """ - 将 资产 序列化成树的结构返回 - """ - filter_queryset: callable - get_queryset: callable - - filterset_fields = ['name', 'address', 'id', 'comment'] - search_fields = ['name', 'address', 'comment'] - - def list(self, request: Request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - - if request.query_params.get('search'): - # 如果用户搜索的条件不精准,会导致返回大量的无意义数据。 - # 这里限制一下返回数据的最大条数 - queryset = queryset[:999] - queryset = sorted(queryset, key=lambda asset: asset.name) - data = self.serialize_assets(queryset, None) - return Response(data=data) diff --git a/apps/perms/api/user_permission/nodes.py b/apps/perms/api/user_permission/nodes.py index eb0d321d5..793ad3858 100644 --- a/apps/perms/api/user_permission/nodes.py +++ b/apps/perms/api/user_permission/nodes.py @@ -1,133 +1,51 @@ # -*- coding: utf-8 -*- # import abc -from rest_framework.request import Request -from rest_framework.response import Response + from rest_framework.generics import ListAPIView -from common.utils import get_logger -from assets.api.mixin import SerializeToTreeNodeMixin +from assets.models import Node +from common.utils import get_logger, lazyproperty from perms import serializers -from perms.hands import User -from perms.utils.user_permission import UserGrantedNodesQueryUtils - -from .mixin import SelfOrPKUserMixin, RebuildTreeMixin +from perms.utils import UserPermNodeUtil +from .mixin import SelfOrPKUserMixin logger = get_logger(__name__) __all__ = [ - 'UserGrantedNodesApi', - 'UserGrantedNodesAsTreeApi', - 'UserGrantedNodeChildrenApi', - 'UserGrantedNodeChildrenAsTreeApi', - 'BaseGrantedNodeAsTreeApi', - 'UserGrantedNodesMixin', + 'UserAllPermedNodesApi', + 'UserPermedNodeChildrenApi', ] -class _GrantedNodeStructApi(ListAPIView, metaclass=abc.ABCMeta): - @property - def user(self): - raise NotImplementedError +class BaseUserPermedNodesApi(SelfOrPKUserMixin, ListAPIView): + serializer_class = serializers.NodePermedSerializer + def get_queryset(self): + if getattr(self, 'swagger_fake_view', False): + return Node.objects.none() + return self.get_nodes() + + @abc.abstractmethod def get_nodes(self): - # 不使用 `get_queryset` 单独定义 `get_nodes` 的原因是 - # `get_nodes` 返回的不一定是 `queryset` - raise NotImplementedError + return [] + + @lazyproperty + def query_node_util(self): + return UserPermNodeUtil(self.user) -class NodeChildrenMixin: - def get_children(self): - raise NotImplementedError - - def get_nodes(self): - nodes = self.get_children() - return nodes - - -class BaseGrantedNodeApi(_GrantedNodeStructApi, metaclass=abc.ABCMeta): - serializer_class = serializers.NodeGrantedSerializer - - def list(self, request, *args, **kwargs): - nodes = self.get_nodes() - serializer = self.get_serializer(nodes, many=True) - return Response(serializer.data) - - -class BaseNodeChildrenApi(NodeChildrenMixin, BaseGrantedNodeApi, metaclass=abc.ABCMeta): - pass - - -class BaseGrantedNodeAsTreeApi(SerializeToTreeNodeMixin, _GrantedNodeStructApi, metaclass=abc.ABCMeta): - def list(self, request: Request, *args, **kwargs): - nodes = self.get_nodes() - nodes = self.serialize_nodes(nodes, with_asset_amount=True) - return Response(data=nodes) - - -class BaseNodeChildrenAsTreeApi(NodeChildrenMixin, BaseGrantedNodeAsTreeApi, metaclass=abc.ABCMeta): - pass - - -class UserGrantedNodeChildrenMixin: - user: User - request: Request - - def get_children(self): - user = self.user - key = self.request.query_params.get('key') - nodes = UserGrantedNodesQueryUtils(user).get_node_children(key) - return nodes - - -class UserGrantedNodesMixin: - """ - 查询用户授权的所有节点 直接授权节点 + 授权资产关联的节点 - """ - user: User - - def get_nodes(self): - utils = UserGrantedNodesQueryUtils(self.user) - nodes = utils.get_whole_tree_nodes() - return nodes - - -# API - - -class UserGrantedNodeChildrenApi( - SelfOrPKUserMixin, - UserGrantedNodeChildrenMixin, - BaseNodeChildrenApi -): - """ 用户授权的节点下的子节点""" - pass - - -class UserGrantedNodeChildrenAsTreeApi( - SelfOrPKUserMixin, - RebuildTreeMixin, - UserGrantedNodeChildrenMixin, - BaseNodeChildrenAsTreeApi -): - """ 用户授权的节点下的子节点树""" - pass - - -class UserGrantedNodesApi( - SelfOrPKUserMixin, - UserGrantedNodesMixin, - BaseGrantedNodeApi -): +class UserAllPermedNodesApi(BaseUserPermedNodesApi): """ 用户授权的节点 """ - pass + + def get_nodes(self): + return self.query_node_util.get_whole_tree_nodes() -class UserGrantedNodesAsTreeApi( - SelfOrPKUserMixin, - RebuildTreeMixin, - UserGrantedNodesMixin, - BaseGrantedNodeAsTreeApi -): - """ 用户授权的节点树 """ - pass +class UserPermedNodeChildrenApi(BaseUserPermedNodesApi): + """ 用户授权的节点下的子节点 """ + + def get_nodes(self): + key = self.request.query_params.get('key') + nodes = self.query_node_util.get_node_children(key) + return nodes diff --git a/apps/perms/api/user_permission/nodes_with_assets.py b/apps/perms/api/user_permission/nodes_with_assets.py deleted file mode 100644 index 20cda7e00..000000000 --- a/apps/perms/api/user_permission/nodes_with_assets.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.conf import settings -from django.db.models import F, Value, CharField -from rest_framework.generics import ListAPIView -from rest_framework.request import Request -from rest_framework.response import Response - -from common.utils import get_logger, get_object_or_none -from common.utils.common import timeit -from common.permissions import IsValidUser - -from assets.models import Asset -from assets.api import SerializeToTreeNodeMixin -from perms.hands import Node -from perms.models import AssetPermission, PermNode -from perms.utils.user_permission import ( - UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids, - UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils, -) - -from .mixin import SelfOrPKUserMixin, RebuildTreeMixin - -logger = get_logger(__name__) - - -class MyGrantedNodesWithAssetsAsTreeApi(SerializeToTreeNodeMixin, ListAPIView): - permission_classes = (IsValidUser,) - - @timeit - def add_ungrouped_resource(self, data: list, nodes_query_utils, assets_query_utils): - if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - return - ungrouped_node = nodes_query_utils.get_ungrouped_node() - - direct_granted_assets = assets_query_utils.get_direct_granted_assets().annotate( - parent_key=Value(ungrouped_node.key, output_field=CharField()) - ).prefetch_related('platform') - - data.extend(self.serialize_nodes([ungrouped_node], with_asset_amount=True)) - data.extend(self.serialize_assets(direct_granted_assets)) - - @timeit - def add_favorite_resource(self, data: list, nodes_query_utils, assets_query_utils): - favorite_node = nodes_query_utils.get_favorite_node() - - favorite_assets = assets_query_utils.get_favorite_assets() - favorite_assets = favorite_assets.annotate( - parent_key=Value(favorite_node.key, output_field=CharField()) - ).prefetch_related('platform') - - data.extend(self.serialize_nodes([favorite_node], with_asset_amount=True)) - data.extend(self.serialize_assets(favorite_assets)) - - @timeit - def add_node_filtered_by_system_user(self, data: list, user, asset_perm_ids): - utils = UserGrantedTreeBuildUtils(user, asset_perm_ids) - nodes = utils.get_whole_tree_nodes() - data.extend(self.serialize_nodes(nodes, with_asset_amount=True)) - - def add_assets(self, data: list, assets_query_utils: UserGrantedAssetsQueryUtils): - if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - all_assets = assets_query_utils.get_direct_granted_nodes_assets() - else: - all_assets = assets_query_utils.get_all_granted_assets() - all_assets = all_assets.annotate(parent_key=F('nodes__key')).prefetch_related('platform') - data.extend(self.serialize_assets(all_assets)) - - def list(self, request: Request, *args, **kwargs): - """ - 此算法依赖 UserGrantedMappingNode - 获取所有授权的节点和资产 - - Node = UserGrantedMappingNode + 授权节点的子节点 - Asset = 授权节点的资产 + 直接授权的资产 - """ - - user = request.user - data = [] - asset_perm_ids = get_user_all_asset_perm_ids(user) - - system_user_id = request.query_params.get('system_user') - if system_user_id: - asset_perm_ids = list(AssetPermission.objects.valid().filter( - id__in=asset_perm_ids, system_users__id=system_user_id, actions__gt=0 - ).values_list('id', flat=True).distinct()) - - nodes_query_utils = UserGrantedNodesQueryUtils(user, asset_perm_ids) - assets_query_utils = UserGrantedAssetsQueryUtils(user, asset_perm_ids) - - self.add_ungrouped_resource(data, nodes_query_utils, assets_query_utils) - self.add_favorite_resource(data, nodes_query_utils, assets_query_utils) - - if system_user_id: - # 有系统用户筛选的需要重新计算树结构 - self.add_node_filtered_by_system_user(data, user, asset_perm_ids) - else: - all_nodes = nodes_query_utils.get_whole_tree_nodes(with_special=False) - data.extend(self.serialize_nodes(all_nodes, with_asset_amount=True)) - - self.add_assets(data, assets_query_utils) - return Response(data=data) - - -class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin, - ListAPIView): - """ - 带资产的授权树 - """ - user: None - - def ensure_key(self): - key = self.request.query_params.get('key', None) - id = self.request.query_params.get('id', None) - - if key is not None: - return key - - node = get_object_or_none(Node, id=id) - if node: - return node.key - - def list(self, request: Request, *args, **kwargs): - user = self.user - key = self.ensure_key() - - nodes_query_utils = UserGrantedNodesQueryUtils(user) - assets_query_utils = UserGrantedAssetsQueryUtils(user) - - nodes = PermNode.objects.none() - assets = Asset.objects.none() - all_tree_nodes = [] - - if not key: - nodes = nodes_query_utils.get_top_level_nodes() - elif key == PermNode.UNGROUPED_NODE_KEY: - assets = assets_query_utils.get_ungroup_assets() - elif key == PermNode.FAVORITE_NODE_KEY: - assets = assets_query_utils.get_favorite_assets() - else: - nodes = nodes_query_utils.get_node_children(key) - assets = assets_query_utils.get_node_assets(key) - assets = assets.prefetch_related('platform') - - tree_nodes = self.serialize_nodes(nodes, with_asset_amount=True) - tree_assets = self.serialize_assets(assets, key) - all_tree_nodes.extend(tree_nodes) - all_tree_nodes.extend(tree_assets) - return Response(data=all_tree_nodes) - - -class UserGrantedNodeChildrenWithAssetsAsTreeApi( - SelfOrPKUserMixin, - RebuildTreeMixin, - GrantedNodeChildrenWithAssetsAsTreeApiMixin -): - """ 用户授权的节点的子节点与资产树 """ - pass diff --git a/apps/perms/api/user_permission/tree/__init__.py b/apps/perms/api/user_permission/tree/__init__.py new file mode 100644 index 000000000..d0e88bc45 --- /dev/null +++ b/apps/perms/api/user_permission/tree/__init__.py @@ -0,0 +1,3 @@ +from .asset import * +from .node import * +from .node_with_asset import * diff --git a/apps/perms/api/user_permission/tree/asset.py b/apps/perms/api/user_permission/tree/asset.py new file mode 100644 index 000000000..081dbc756 --- /dev/null +++ b/apps/perms/api/user_permission/tree/asset.py @@ -0,0 +1,48 @@ +from django.conf import settings +from rest_framework.response import Response + +from assets.models import Asset +from assets.api import SerializeToTreeNodeMixin +from common.utils import get_logger + +from ..assets import UserDirectPermedAssetsApi +from .mixin import RebuildTreeMixin + +logger = get_logger(__name__) + + +__all__ = [ + 'UserDirectPermedAssetsAsTreeApi', + 'UserUngroupAssetsAsTreeApi', +] + + +class AssetTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin): + """ 将资产序列化成树节点的结构返回 """ + filter_queryset: callable + get_queryset: callable + + ordering = ('name',) + filterset_fields = ('id', 'name', 'address', 'comment') + search_fields = ('name', 'address', 'comment') + + def list(self, request, *args, **kwargs): + assets = self.filter_queryset(self.get_queryset()) + if request.query_params.get('search'): + """ 限制返回数量, 搜索的条件不精准时,会返回大量的无意义数据 """ + assets = assets[:999] + data = self.serialize_assets(assets, None) + return Response(data=data) + + +class UserDirectPermedAssetsAsTreeApi(AssetTreeMixin, UserDirectPermedAssetsApi): + """ 用户 '直接授权的资产' 作为树 """ + pass + + +class UserUngroupAssetsAsTreeApi(UserDirectPermedAssetsAsTreeApi): + """ 用户 '未分组节点的资产(直接授权的资产)' 作为树 """ + def get_assets(self): + if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + return super().get_assets() + return Asset.objects.none() diff --git a/apps/perms/api/user_permission/tree/mixin.py b/apps/perms/api/user_permission/tree/mixin.py new file mode 100644 index 000000000..c0cafe37c --- /dev/null +++ b/apps/perms/api/user_permission/tree/mixin.py @@ -0,0 +1,40 @@ +from django.core.cache import cache + +from rest_framework.request import Request + +from common.http import is_true +from common.utils import lazyproperty +from perms.utils import UserPermTreeRefreshUtil +from users.models import User + +__all__ = ['RebuildTreeMixin'] + + +class RebuildTreeMixin: + user: User + request: Request + + def get(self, request, *args, **kwargs): + UserPermTreeRefreshUtil(self.user).refresh_if_need(force=self.is_force_refresh_tree) + return super().get(request, *args, **kwargs) + + @lazyproperty + def is_force_refresh_tree(self): + force = is_true(self.request.query_params.get('rebuild_tree')) + if not force: + force = self.compute_is_force_refresh() + return force + + def compute_is_force_refresh(self): + """ 5s 内连续刷新三次转为强制刷新 """ + force_timeout = 5 + force_max_count = 3 + force_cache_key = '{user_id}:{path}'.format(user_id=self.user.id, path=self.request.path) + count = cache.get(force_cache_key, 1) + if count >= force_max_count: + force = True + cache.delete(force_cache_key) + else: + force = False + cache.set(force_cache_key, count+1, force_timeout) + return force diff --git a/apps/perms/api/user_permission/tree/node.py b/apps/perms/api/user_permission/tree/node.py new file mode 100644 index 000000000..5872753c5 --- /dev/null +++ b/apps/perms/api/user_permission/tree/node.py @@ -0,0 +1,39 @@ +from rest_framework.response import Response + +from assets.api import SerializeToTreeNodeMixin +from common.utils import get_logger + +from .mixin import RebuildTreeMixin +from ..nodes import ( + UserAllPermedNodesApi, + UserPermedNodeChildrenApi, +) + +logger = get_logger(__name__) + +__all__ = [ + 'UserAllPermedNodesAsTreeApi', + 'UserPermedNodeChildrenAsTreeApi', +] + + +class NodeTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin): + filter_queryset: callable + get_queryset: callable + + def list(self, request, *args, **kwargs): + nodes = self.filter_queryset(self.get_queryset()) + data = self.serialize_nodes(nodes, with_asset_amount=True) + return Response(data) + + +class UserAllPermedNodesAsTreeApi(NodeTreeMixin, UserAllPermedNodesApi): + """ 用户 '授权的节点' 作为树 """ + pass + + +class UserPermedNodeChildrenAsTreeApi(NodeTreeMixin, UserPermedNodeChildrenApi): + """ 用户授权的节点下的子节点树 """ + pass + + diff --git a/apps/perms/api/user_permission/tree/node_with_asset.py b/apps/perms/api/user_permission/tree/node_with_asset.py new file mode 100644 index 000000000..9aa73963b --- /dev/null +++ b/apps/perms/api/user_permission/tree/node_with_asset.py @@ -0,0 +1,187 @@ +import abc +from urllib.parse import parse_qsl + +from django.conf import settings +from django.db.models import F, Value, CharField +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.generics import ListAPIView +from rest_framework.generics import get_object_or_404 +from rest_framework.exceptions import PermissionDenied, NotFound + +from assets.utils import KubernetesTree +from assets.models import Asset, Account +from assets.const import AliasAccount +from assets.api import SerializeToTreeNodeMixin +from authentication.models import ConnectionToken +from common.utils import get_object_or_none, lazyproperty +from common.utils.common import timeit +from perms.hands import Node +from perms.models import PermNode +from perms.utils import PermAccountUtil, UserPermNodeUtil, AssetPermissionUtil +from perms.utils import UserPermAssetUtil +from .mixin import RebuildTreeMixin +from ..mixin import SelfOrPKUserMixin + +__all__ = [ + 'UserGrantedK8sAsTreeApi', + 'UserPermedNodesWithAssetsAsTreeApi', + 'UserPermedNodeChildrenWithAssetsAsTreeApi' +] + + +class BaseUserNodeWithAssetAsTreeApi( + SelfOrPKUserMixin, RebuildTreeMixin, + SerializeToTreeNodeMixin, ListAPIView +): + + def list(self, request, *args, **kwargs): + nodes, assets = self.get_nodes_assets() + tree_nodes = self.serialize_nodes(nodes, with_asset_amount=True) + tree_assets = self.serialize_assets(assets, node_key=self.node_key_for_serialize_assets) + data = list(tree_nodes) + list(tree_assets) + return Response(data=data) + + @abc.abstractmethod + def get_nodes_assets(self): + return [], [] + + @lazyproperty + def node_key_for_serialize_assets(self): + return None + + +class UserPermedNodesWithAssetsAsTreeApi(BaseUserNodeWithAssetAsTreeApi): + query_node_util: UserPermNodeUtil + query_asset_util: UserPermAssetUtil + + def get_nodes_assets(self): + self.query_node_util = UserPermNodeUtil(self.request.user) + self.query_asset_util = UserPermAssetUtil(self.request.user) + ung_nodes, ung_assets = self._get_nodes_assets_for_ungrouped() + fav_nodes, fav_assets = self._get_nodes_assets_for_favorite() + all_nodes, all_assets = self._get_nodes_assets_for_all() + nodes = list(ung_nodes) + list(fav_nodes) + list(all_nodes) + assets = list(ung_assets) + list(fav_assets) + list(all_assets) + return nodes, assets + + @timeit + def _get_nodes_assets_for_ungrouped(self): + if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + return [], [] + node = self.query_node_util.get_ungrouped_node() + assets = self.query_asset_util.get_ungroup_assets() + assets = assets.annotate(parent_key=Value(node.key, output_field=CharField())) \ + .prefetch_related('platform') + return [node], assets + + @timeit + def _get_nodes_assets_for_favorite(self): + node = self.query_node_util.get_favorite_node() + assets = self.query_asset_util.get_favorite_assets() + assets = assets.annotate(parent_key=Value(node.key, output_field=CharField())) \ + .prefetch_related('platform') + return [node], assets + + def _get_nodes_assets_for_all(self): + nodes = self.query_node_util.get_whole_tree_nodes(with_special=False) + if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + assets = self.query_asset_util.get_perm_nodes_assets() + else: + assets = self.query_asset_util.get_all_assets() + assets = assets.annotate(parent_key=F('nodes__key')).prefetch_related('platform') + return nodes, assets + + +class UserPermedNodeChildrenWithAssetsAsTreeApi(BaseUserNodeWithAssetAsTreeApi): + """ 用户授权的节点的子节点与资产树 """ + + # 默认展开的节点key + default_unfolded_node_key = None + + def get_nodes_assets(self): + query_node_util = UserPermNodeUtil(self.user) + query_asset_util = UserPermAssetUtil(self.user) + node_key = self.query_node_key + if not node_key: + nodes, unfolded_node = query_node_util.get_top_level_nodes(with_unfolded_node=True) + if unfolded_node: + """ 默认展开的节点, 获取根节点下的资产 """ + assets = query_asset_util.get_node_assets(key=unfolded_node.key) + self.default_unfolded_node_key = unfolded_node.key + else: + assets = Asset.objects.none() + elif node_key == PermNode.UNGROUPED_NODE_KEY: + nodes = PermNode.objects.none() + assets = query_asset_util.get_ungroup_assets() + elif node_key == PermNode.FAVORITE_NODE_KEY: + nodes = PermNode.objects.none() + assets = query_asset_util.get_favorite_assets() + else: + nodes = query_node_util.get_node_children(node_key) + assets = query_asset_util.get_node_assets(key=node_key) + assets = assets.prefetch_related('platform') + return nodes, assets + + @lazyproperty + def query_node_key(self): + node_key = self.request.query_params.get('key', None) + if node_key is None: + node_id = self.request.query_params.get('id', None) + node = get_object_or_none(Node, id=node_id) + node_key = getattr(node, 'key', None) + return node_key + + @lazyproperty + def node_key_for_serialize_assets(self): + return self.query_node_key or self.default_unfolded_node_key + + +class UserGrantedK8sAsTreeApi(SelfOrPKUserMixin, ListAPIView): + """ 用户授权的K8s树 """ + + def get_token(self): + token_id = self.request.query_params.get('token') + token = get_object_or_404(ConnectionToken, pk=token_id) + if token.is_expired: + raise PermissionDenied('Token is expired') + token.renewal() + return token + + def get_account_secret(self, token: ConnectionToken): + util = PermAccountUtil() + accounts = util.get_permed_accounts_for_user(self.user, token.asset) + account_username = token.account + accounts = filter(lambda x: x.username == account_username, accounts) + accounts = list(accounts) + if not accounts: + raise NotFound('Account is not found') + account = accounts[0] + if account.username in [ + AliasAccount.INPUT, AliasAccount.USER + ]: + return token.input_secret + else: + return account.secret + + @staticmethod + def get_namespace_and_pod(key): + namespace_and_pod = dict(parse_qsl(key)) + pod = namespace_and_pod.get('pod') + namespace = namespace_and_pod.get('namespace') + return namespace, pod + + def list(self, request: Request, *args, **kwargs): + token = self.get_token() + asset = token.asset + secret = self.get_account_secret(token) + key = self.request.query_params.get('key') + namespace, pod = self.get_namespace_and_pod(key) + + tree = [] + k8s_tree_instance = KubernetesTree(asset, secret) + if not any([namespace, pod]) and not key: + asset_node = k8s_tree_instance.as_asset_tree_node() + tree.append(asset_node) + tree.extend(k8s_tree_instance.async_tree_node(namespace, pod)) + return Response(data=tree) diff --git a/apps/perms/apps.py b/apps/perms/apps.py index f9d1e6f42..432c194cd 100644 --- a/apps/perms/apps.py +++ b/apps/perms/apps.py @@ -10,5 +10,5 @@ class PermsConfig(AppConfig): def ready(self): super().ready() - # from . import signal_handlers + from . import signal_handlers from . import notifications diff --git a/apps/perms/filters.py b/apps/perms/filters.py index e64e919ea..8743c38be 100644 --- a/apps/perms/filters.py +++ b/apps/perms/filters.py @@ -1,7 +1,6 @@ from django_filters import rest_framework as filters from django.db.models import QuerySet, Q -from common.db.models import UnionQuerySet from common.drf.filters import BaseFilterSet from common.utils import get_object_or_none from users.models import User, UserGroup @@ -13,15 +12,15 @@ class PermissionBaseFilter(BaseFilterSet): is_valid = filters.BooleanFilter(method='do_nothing') user_id = filters.UUIDFilter(method='do_nothing') username = filters.CharFilter(method='do_nothing') - system_user_id = filters.UUIDFilter(method='do_nothing') - system_user = filters.CharFilter(method='do_nothing') + account_id = filters.UUIDFilter(method='do_nothing') + account = filters.CharFilter(method='do_nothing') user_group_id = filters.UUIDFilter(method='do_nothing') user_group = filters.CharFilter(method='do_nothing') all = filters.BooleanFilter(method='do_nothing') class Meta: fields = ( - 'user_id', 'username', 'system_user_id', 'system_user', + 'user_id', 'username', 'account_id', 'account', 'user_group_id', 'user_group', 'name', 'all', 'is_valid', ) @@ -169,10 +168,10 @@ class AssetPermissionFilter(PermissionBaseFilter): inherit_all_node_ids = Node.objects.filter(key__in=inherit_all_node_keys).values_list('id', flat=True) inherit_all_node_ids = list(inherit_all_node_ids) - qs1 = queryset.filter(assets__in=asset_ids).distinct() - qs2 = queryset.filter(nodes__in=inherit_all_node_ids).distinct() - - qs = UnionQuerySet(qs1, qs2) + qs1_ids = queryset.filter(assets__in=asset_ids).distinct().values_list('id', flat=True) + qs2_ids = queryset.filter(nodes__in=inherit_all_node_ids).distinct().values_list('id', flat=True) + qs_ids = list(qs1_ids) + list(qs2_ids) + qs = queryset.filter(id__in=qs_ids) return qs def filter_effective(self, queryset): diff --git a/apps/perms/locks.py b/apps/perms/locks.py index 96c766fb8..77babf7f8 100644 --- a/apps/perms/locks.py +++ b/apps/perms/locks.py @@ -2,7 +2,7 @@ from common.utils.lock import DistributedLock class UserGrantedTreeRebuildLock(DistributedLock): - name_template = 'perms.user.asset.node.tree.rebuid.' + name_template = 'perms.user.asset.node.tree.rebuild.' def __init__(self, user_id): name = self.name_template.format(user_id=user_id) diff --git a/apps/perms/migrations/0034_auto_20221220_1956.py b/apps/perms/migrations/0034_auto_20221220_1956.py new file mode 100644 index 000000000..41f6528d1 --- /dev/null +++ b/apps/perms/migrations/0034_auto_20221220_1956.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0033_alter_assetpermission_actions'), + ] + + operations = [ + migrations.AddField( + model_name='assetpermission', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='assetpermission', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='assetpermission', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='assetpermission', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='assetpermission', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='userassetgrantedtreenoderelation', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='userassetgrantedtreenoderelation', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index 9041990f2..63bd0b025 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -3,4 +3,3 @@ from .asset_permission import * from .perm_node import * -from .perm_token import * diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index aa61ffe14..2af68832c 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,17 +1,19 @@ import logging -import uuid from django.db import models from django.db.models import Q from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from users.models import User from assets.models import Asset, Account -from common.db.models import UnionQuerySet -from common.utils import date_expired_default +from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import OrgManager -from orgs.mixins.models import OrgModelMixin +from common.utils import date_expired_default +from common.utils.timezone import local_now + from perms.const import ActionChoices +from assets.const import AliasAccount __all__ = ['AssetPermission', 'ActionChoices'] @@ -37,7 +39,7 @@ class AssetPermissionQuerySet(models.QuerySet): def filter_by_accounts(self, accounts): q = Q(accounts__contains=list(accounts)) | \ - Q(accounts__contains=Account.AliasAccount.ALL.value) + Q(accounts__contains=AliasAccount.ALL.value) return self.filter(q) @@ -45,9 +47,12 @@ class AssetPermissionManager(OrgManager): def valid(self): return self.get_queryset().valid() + def get_expired_permissions(self): + now = local_now() + return self.get_queryset().filter(Q(date_start__lte=now) | Q(date_expired__gte=now)) -class AssetPermission(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) + +class AssetPermission(JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) users = models.ManyToManyField( 'users.User', related_name='%(class)ss', blank=True, verbose_name=_("User") @@ -68,11 +73,8 @@ class AssetPermission(OrgModelMixin): date_expired = models.DateTimeField( default=date_expired_default, db_index=True, verbose_name=_('Date expired') ) - comment = models.TextField(verbose_name=_('Comment'), blank=True) is_active = models.BooleanField(default=True, verbose_name=_('Active')) from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) - created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) objects = AssetPermissionManager.from_queryset(AssetPermissionQuerySet)() @@ -103,9 +105,10 @@ class AssetPermission(OrgModelMixin): group_ids = self.user_groups.all().values_list('id', flat=True) user_ids = list(user_ids) group_ids = list(group_ids) - qs1 = User.objects.filter(id__in=user_ids).distinct() - qs2 = User.objects.filter(groups__id__in=group_ids).distinct() - qs = UnionQuerySet(qs1, qs2) + qs1_ids = User.objects.filter(id__in=user_ids).distinct().values_list('id', flat=True) + qs2_ids = User.objects.filter(groups__id__in=group_ids).distinct().values_list('id', flat=True) + qs_ids = list(qs1_ids) + list(qs2_ids) + qs = User.objects.filter(id__in=qs_ids) return qs def get_all_assets(self, flat=False): @@ -125,9 +128,22 @@ class AssetPermission(OrgModelMixin): """ asset_ids = self.get_all_assets(flat=True) q = Q(asset_id__in=asset_ids) - if Account.AliasAccount.ALL not in self.accounts: + if AliasAccount.ALL not in self.accounts: q &= Q(username__in=self.accounts) accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username') if not flat: return accounts return accounts.values_list('id', flat=True) + + @classmethod + def get_all_users_for_perms(cls, perm_ids, flat=False): + user_ids = cls.users.through.objects.filter(assetpermission_id__in=perm_ids) \ + .values_list('user_id', flat=True).distinct() + group_ids = cls.user_groups.through.objects.filter(assetpermission_id__in=perm_ids) \ + .values_list('usergroup_id', flat=True).distinct() + group_user_ids = User.groups.through.objects.filter(usergroup_id__in=group_ids) \ + .values_list('user_id', flat=True).distinct() + user_ids = set(user_ids) | set(group_user_ids) + if flat: + return user_ids + return User.objects.filter(id__in=user_ids) diff --git a/apps/perms/models/perm_node.py b/apps/perms/models/perm_node.py index ce061297e..1db0f2fe7 100644 --- a/apps/perms/models/perm_node.py +++ b/apps/perms/models/perm_node.py @@ -1,28 +1,39 @@ - -from django.utils.translation import ugettext_lazy as _ from django.db import models from django.db.models import F, TextChoices +from django.utils.translation import ugettext_lazy as _ -from common.utils import lazyproperty -from common.db.models import BaseCreateUpdateModel from assets.models import Asset, Node, FamilyMixin, Account -from orgs.mixins.models import OrgModelMixin +from common.utils import lazyproperty +from orgs.mixins.models import JMSOrgBaseModel -class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, BaseCreateUpdateModel): - class NodeFrom(TextChoices): - granted = 'granted', 'Direct node granted' - child = 'child', 'Have children node' - asset = 'asset', 'Direct asset granted' +class NodeFrom(TextChoices): + granted = 'granted', 'Direct node granted' + child = 'child', 'Have children node' + asset = 'asset', 'Direct asset granted' + +class UserAssetGrantedTreeNodeRelation(FamilyMixin, JMSOrgBaseModel): + NodeFrom = NodeFrom + + id = models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name=_('ID') + ) user = models.ForeignKey('users.User', db_constraint=False, on_delete=models.CASCADE) - node = models.ForeignKey('assets.Node', default=None, on_delete=models.CASCADE, - db_constraint=False, null=False, related_name='granted_node_rels') + node = models.ForeignKey( + 'assets.Node', default=None, on_delete=models.CASCADE, db_constraint=False, null=False, + related_name='granted_node_rels' + ) node_key = models.CharField(max_length=64, verbose_name=_("Key"), db_index=True) - node_parent_key = models.CharField(max_length=64, default='', verbose_name=_('Parent key'), - db_index=True) + node_parent_key = models.CharField( + max_length=64, default='', verbose_name=_('Parent key'), db_index=True + ) node_from = models.CharField(choices=NodeFrom.choices, max_length=16, db_index=True) node_assets_amount = models.IntegerField(default=0) + comment = '' + + def __str__(self): + return f'{self.user}|{self.node}' @property def key(self): @@ -33,19 +44,20 @@ class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, BaseCreateUpd return self.node_parent_key @classmethod - def get_node_granted_status(cls, user, key): + def get_node_from_with_node(cls, user, key): ancestor_keys = set(cls.get_node_ancestor_keys(key, with_self=True)) - ancestor_rel_nodes = cls.objects.filter(user=user, node_key__in=ancestor_keys) - - for rel_node in ancestor_rel_nodes: - if rel_node.key == key: - return rel_node.node_from, rel_node - if rel_node.node_from == cls.NodeFrom.granted: - return cls.NodeFrom.granted, None + ancestor_nodes = cls.objects.filter(user=user, node_key__in=ancestor_keys) + for node in ancestor_nodes: + if node.key == key: + return node.node_from, node + if node.node_from == cls.NodeFrom.granted: + return node.node_from, None return '', None class PermNode(Node): + NodeFrom = NodeFrom + class Meta: proxy = True ordering = [] @@ -64,6 +76,9 @@ class PermNode(Node): 'node_from': F('granted_node_rels__node_from') } + def __str__(self): + return f'{self.name}' + def use_granted_assets_amount(self): self.assets_amount = self.granted_assets_amount @@ -86,15 +101,16 @@ class PermNode(Node): node.assets_amount = assets_amount return node - def get_granted_status(self, user): - status, rel_node = UserAssetGrantedTreeNodeRelation.get_node_granted_status(user, self.key) - self.node_from = status - if rel_node: - self.granted_assets_amount = rel_node.node_assets_amount - return status + def compute_node_from_and_assets_amount(self, user): + node_from, node = UserAssetGrantedTreeNodeRelation.get_node_from_with_node( + user, self.key + ) + self.node_from = node_from + if node: + self.granted_assets_amount = node.node_assets_amount def save(self): - # 这是个只读 Model + """ 这是个只读 Model """ raise NotImplementedError diff --git a/apps/perms/models/perm_token.py b/apps/perms/models/perm_token.py deleted file mode 100644 index 368750c63..000000000 --- a/apps/perms/models/perm_token.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class PermToken(models.Model): - """ - 1. 用完失效 - 2. 仅用于授权,不用于认证 - 3. 存 redis 就行 - 4. 有效期 5 分钟 - """ - user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User')) - asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) - account = models.CharField(max_length=128, verbose_name=_('Account')) - secret = models.CharField(max_length=1024, verbose_name=_('Secret')) - protocol = models.CharField(max_length=32, verbose_name=_('Protocol')) - connect_method = models.CharField(max_length=32, verbose_name=_('Connect method')) - actions = models.IntegerField(verbose_name=_('Actions')) - - class Meta: - abstract = True diff --git a/apps/perms/pagination.py b/apps/perms/pagination.py index 622306924..ca20b68d4 100644 --- a/apps/perms/pagination.py +++ b/apps/perms/pagination.py @@ -16,7 +16,7 @@ class GrantedAssetPaginationBase(AssetPaginationBase): self._user = view.user -class NodeGrantedAssetPagination(GrantedAssetPaginationBase): +class NodePermedAssetPagination(GrantedAssetPaginationBase): def get_count_from_nodes(self, queryset): node = getattr(self._view, 'pagination_node', None) if node: @@ -29,7 +29,7 @@ class NodeGrantedAssetPagination(GrantedAssetPaginationBase): return None -class AllGrantedAssetPagination(GrantedAssetPaginationBase): +class AllPermedAssetPagination(GrantedAssetPaginationBase): def get_count_from_nodes(self, queryset): if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: return None diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index fc2bf1cf3..673b567c5 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -20,12 +20,12 @@ class ActionChoicesField(BitChoicesField): class AssetPermissionSerializer(BulkOrgResourceModelSerializer): - users = ObjectRelatedField(queryset=User.objects, many=True, required=False) + users = ObjectRelatedField(queryset=User.objects, many=True, required=False, label=_('User')) user_groups = ObjectRelatedField( - queryset=UserGroup.objects, many=True, required=False + queryset=UserGroup.objects, many=True, required=False, label=_('User group') ) - assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False) - nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False) + assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False, label=_('Asset')) + nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False, label=_('Node')) actions = ActionChoicesField(required=False, allow_null=True, label=_("Actions")) is_valid = serializers.BooleanField(read_only=True, label=_("Is valid")) is_expired = serializers.BooleanField(read_only=True, label=_("Is expired")) diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index a85770a50..9b4ad26f0 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -11,12 +11,12 @@ from common.drf.fields import ObjectRelatedField, LabeledChoiceField from perms.serializers.permission import ActionChoicesField __all__ = [ - 'NodeGrantedSerializer', 'AssetGrantedSerializer', + 'NodePermedSerializer', 'AssetPermedSerializer', 'AccountsPermedSerializer' ] -class AssetGrantedSerializer(serializers.ModelSerializer): +class AssetPermedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) @@ -26,15 +26,14 @@ class AssetGrantedSerializer(serializers.ModelSerializer): class Meta: model = Asset only_fields = [ - "id", "name", "address", - 'domain', 'platform', + "id", "name", "address", 'domain', 'platform', "comment", "org_id", "is_active", ] fields = only_fields + ['protocols', 'category', 'type', 'specific'] + ['org_name'] read_only_fields = fields -class NodeGrantedSerializer(serializers.ModelSerializer): +class NodePermedSerializer(serializers.ModelSerializer): class Meta: model = Node fields = [ @@ -48,6 +47,8 @@ class AccountsPermedSerializer(serializers.ModelSerializer): class Meta: model = Account - fields = ['id', 'name', 'has_username', 'username', - 'has_secret', 'secret_type', 'actions'] + fields = [ + 'alias', 'name', 'username', 'has_username', + 'has_secret', 'secret_type', 'actions' + ] read_only_fields = fields diff --git a/apps/perms/signal_handlers/__init__.py b/apps/perms/signal_handlers/__init__.py index 6a8ef9467..02faddd04 100644 --- a/apps/perms/signal_handlers/__init__.py +++ b/apps/perms/signal_handlers/__init__.py @@ -1,2 +1,2 @@ -from . import asset_permission -from . import refresh_perms +from .asset_permission import * +from .refresh_perms import * diff --git a/apps/perms/signal_handlers/refresh_perms.py b/apps/perms/signal_handlers/refresh_perms.py index 2e66c4475..88a5fe674 100644 --- a/apps/perms/signal_handlers/refresh_perms.py +++ b/apps/perms/signal_handlers/refresh_perms.py @@ -5,12 +5,12 @@ from django.dispatch import receiver from users.models import User, UserGroup from assets.models import Asset -from orgs.utils import current_org, tmp_to_org -from common.utils import get_logger +from common.utils import get_logger, get_object_or_none from common.exceptions import M2MReverseNotAllowed from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR + from perms.models import AssetPermission -from perms.utils.user_permission import UserGrantedTreeRefreshController +from perms.utils import UserPermTreeExpireUtil logger = get_logger(__file__) @@ -21,10 +21,7 @@ def on_user_group_delete(sender, instance: UserGroup, using, **kwargs): exists = AssetPermission.user_groups.through.objects.filter(usergroup_id=instance.id).exists() if not exists: return - - org_id = instance.org_id - user_ids = UserGroup.users.through.objects.filter(usergroup_id=instance.id).values_list('user_id', flat=True) - UserGrantedTreeRefreshController.add_need_refresh_orgs_for_users([org_id], list(user_ids)) + UserPermTreeExpireUtil().expire_perm_tree_for_user_group(instance) @receiver(m2m_changed, sender=User.groups.through) @@ -41,39 +38,34 @@ def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs): group = UserGroup.objects.get(id=list(group_ids)[0]) org_id = group.org_id - exists = AssetPermission.user_groups.through.objects.filter(usergroup_id__in=group_ids).exists() - if not exists: + has_group_perm = AssetPermission.user_groups.through.objects\ + .filter(usergroup_id__in=group_ids).exists() + if not has_group_perm: return - org_ids = [org_id] - UserGrantedTreeRefreshController.add_need_refresh_orgs_for_users(org_ids, user_ids) + UserPermTreeExpireUtil().expire_perm_tree_for_users_orgs(user_ids, [org_id]) @receiver([pre_delete], sender=AssetPermission) def on_asset_perm_pre_delete(sender, instance, **kwargs): - # 授权删除之前,查出所有相关用户 - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids([instance.id]) + UserPermTreeExpireUtil().expire_perm_tree_for_perms([instance.id]) @receiver([pre_save], sender=AssetPermission) def on_asset_perm_pre_save(sender, instance, **kwargs): - try: - old = AssetPermission.objects.get(id=instance.id) - - if old.is_valid != instance.is_valid: - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids([instance.id]) - except AssetPermission.DoesNotExist: - pass + old = get_object_or_none(AssetPermission, pk=instance.id) + if not old: + return + if old.is_valid == instance.is_valid: + return + UserPermTreeExpireUtil().expire_perm_tree_for_perms([instance.id]) @receiver([post_save], sender=AssetPermission) def on_asset_perm_post_save(sender, instance, created, **kwargs): if not created: return - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids([instance.id]) + UserPermTreeExpireUtil().expire_perm_tree_for_perms([instance.id]) def need_rebuild_mapping_node(action): @@ -82,69 +74,52 @@ def need_rebuild_mapping_node(action): @receiver(m2m_changed, sender=AssetPermission.nodes.through) def on_permission_nodes_changed(sender, instance, action, reverse, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if not need_rebuild_mapping_node(action): return - - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids([instance.id]) + if reverse: + raise M2MReverseNotAllowed + UserPermTreeExpireUtil().expire_perm_tree_for_perms([instance.id]) @receiver(m2m_changed, sender=AssetPermission.assets.through) def on_permission_assets_changed(sender, instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if not need_rebuild_mapping_node(action): return - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids([instance.id]) + if reverse: + raise M2MReverseNotAllowed + UserPermTreeExpireUtil().expire_perm_tree_for_perms([instance.id]) @receiver(m2m_changed, sender=AssetPermission.users.through) def on_asset_permission_users_changed(sender, action, reverse, instance, pk_set, **kwargs): if reverse: raise M2MReverseNotAllowed - if not need_rebuild_mapping_node(action): return - - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_orgs_for_users( - [current_org.id], pk_set - ) + user_ids = pk_set + UserPermTreeExpireUtil().expire_perm_tree_for_users_orgs(user_ids, [instance.org.id]) @receiver(m2m_changed, sender=AssetPermission.user_groups.through) def on_asset_permission_user_groups_changed(sender, instance, action, pk_set, reverse, **kwargs): + if not need_rebuild_mapping_node(action): + return if reverse: raise M2MReverseNotAllowed - if not need_rebuild_mapping_node(action): - return - - user_ids = User.groups.through.objects.filter(usergroup_id__in=pk_set) \ - .values_list('user_id', flat=True) \ - .distinct() - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_orgs_for_users( - [current_org.id], user_ids - ) + group_ids = pk_set + UserPermTreeExpireUtil().expire_perm_tree_for_user_groups_orgs(group_ids, [instance.org.id]) @receiver(m2m_changed, sender=Asset.nodes.through) def on_node_asset_change(action, instance, reverse, pk_set, **kwargs): if not need_rebuild_mapping_node(action): return - if reverse: - asset_pk_set = pk_set - node_pk_set = [instance.id] + asset_ids = pk_set + node_ids = [instance.id] else: - asset_pk_set = [instance.id] - node_pk_set = pk_set + asset_ids = [instance.id] + node_ids = pk_set - with tmp_to_org(instance.org): - UserGrantedTreeRefreshController.add_need_refresh_on_nodes_assets_relate_change(node_pk_set, asset_pk_set) + UserPermTreeExpireUtil().expire_perm_tree_for_nodes_assets(node_ids, asset_ids) diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 564e5657e..57b9f48bd 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -7,16 +7,18 @@ from django.db.transaction import atomic from django.conf import settings from celery import shared_task +from ops.celery.decorator import register_as_period_task from orgs.utils import tmp_to_root_org from common.utils import get_logger -from common.utils.timezone import local_now, dt_formatter, dt_parser +from common.utils.timezone import local_now, dt_parser from common.const.crontab import CRONTAB_AT_AM_TEN -from ops.celery.decorator import register_as_period_task -from perms.notifications import ( - PermedAssetsWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg, -) + from perms.models import AssetPermission -from perms.utils.user_permission import UserGrantedTreeRefreshController +from perms.utils import UserPermTreeExpireUtil +from perms.notifications import ( + PermedAssetsWillExpireUserMsg, + AssetPermsWillExpireForOrgAdminMsg, +) logger = get_logger(__file__) @@ -26,33 +28,11 @@ logger = get_logger(__file__) @atomic() @tmp_to_root_org() def check_asset_permission_expired(): - """ - 这里的任务要足够短,不要影响周期任务 - """ - from settings.models import Setting - - setting_name = 'last_asset_perm_expired_check' - - end = local_now() - default_start = end - timedelta(days=36000) # Long long ago in china - - defaults = {'value': dt_formatter(default_start)} - setting, created = Setting.objects.get_or_create( - name=setting_name, defaults=defaults - ) - if created: - start = default_start - else: - start = dt_parser(setting.value) - setting.value = dt_formatter(end) - setting.save() - - asset_perm_ids = AssetPermission.objects.filter( - date_expired__gte=start, date_expired__lte=end - ).distinct().values_list('id', flat=True) - asset_perm_ids = list(asset_perm_ids) - logger.info(f'>>> checking {start} to {end} have {asset_perm_ids} expired') - UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids_cross_orgs(asset_perm_ids) + """ 这里的任务要足够短,不要影响周期任务 """ + perms = AssetPermission.objects.get_expired_permissions() + perm_ids = list(perms.distinct().values_list('id', flat=True)) + logger.info(f'Checking expired permissions: {perm_ids}') + UserPermTreeExpireUtil().expire_perm_tree_for_perms(perm_ids) @register_as_period_task(crontab=CRONTAB_AT_AM_TEN) diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py index c3a555375..63ed4f636 100644 --- a/apps/perms/urls/user_permission.py +++ b/apps/perms/urls/user_permission.py @@ -4,40 +4,46 @@ from .. import api user_permission_urlpatterns = [ # such as: my | self | user.id - # assets path('/assets/', api.UserAllPermedAssetsApi.as_view(), - name='user-assets'), + name='user-all-assets'), + path('/nodes/ungrouped/assets/', api.UserDirectPermedAssetsApi.as_view(), + name='user-direct-assets'), + path('/nodes/favorite/assets/', api.UserFavoriteAssetsApi.as_view(), + name='user-favorite-assets'), + path('/nodes//assets/', api.UserPermedNodeAssetsApi.as_view(), + name='user-node-assets'), + + # nodes + path('/nodes/', api.UserAllPermedNodesApi.as_view(), + name='user-all-nodes'), + path('/nodes/children/', api.UserPermedNodeChildrenApi.as_view(), + name='user-node-children'), + + # tree-asset path('/assets/tree/', api.UserDirectPermedAssetsAsTreeApi.as_view(), - name='user-assets-as-tree'), + name='user-direct-assets-as-tree'), path('/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(), name='user-ungroup-assets-as-tree'), - # nodes - path('/nodes/', api.UserGrantedNodesApi.as_view(), - name='user-nodes'), - path('/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), - name='user-nodes-as-tree'), - path('/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), - name='user-nodes-children'), - path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), - name='user-nodes-children-as-tree'), - - # node-assets - path('/nodes//assets/', api.UserPermedNodeAssetsApi.as_view(), - name='user-node-assets'), - path('/nodes/ungrouped/assets/', api.UserDirectPermedAssetsApi.as_view(), - name='user-ungrouped-assets'), - path('/nodes/favorite/assets/', api.UserFavoriteAssetsApi.as_view(), - name='user-ungrouped-assets'), + # tree-node,不包含资产 + path('/nodes/tree/', api.UserAllPermedNodesAsTreeApi.as_view(), + name='user-all-nodes-as-tree'), + path('/nodes/children/tree/', api.UserPermedNodeChildrenAsTreeApi.as_view(), + name='user-node-children-as-tree'), + # tree-node-with-asset + # 异步树 path('/nodes/children-with-assets/tree/', - api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), - name='user-nodes-children-with-assets-as-tree'), - - path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(), - name='my-nodes-with-assets-as-tree'), - + api.UserPermedNodeChildrenWithAssetsAsTreeApi.as_view(), + name='user-node-children-with-assets-as-tree'), + # 同步树 + path('/nodes/all-with-assets/tree/', + api.UserPermedNodesWithAssetsAsTreeApi.as_view(), + name='user-nodes-with-assets-as-tree'), + path('/nodes/children-with-k8s/tree/', + api.UserGrantedK8sAsTreeApi.as_view(), + name='user-nodes-children-with-k8s-as-tree'), # accounts path('/assets//accounts/', api.UserPermedAssetAccountsApi.as_view(), name='user-permed-asset-accounts'), diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py index fc2a94e88..808bdd1b8 100644 --- a/apps/perms/utils/__init__.py +++ b/apps/perms/utils/__init__.py @@ -1,3 +1,4 @@ from .permission import * -from .user_permission import * from .account import * +from .user_perm_tree import * +from .user_perm import * diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index 7c0caf988..bdec2b934 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,6 +1,7 @@ from collections import defaultdict from assets.models import Account +from assets.const import AliasAccount from .permission import AssetPermissionUtil __all__ = ['PermAccountUtil'] @@ -16,7 +17,7 @@ class PermAccountUtil(AssetPermissionUtil): :param account_name: 可能是 @USER @INPUT 字符串 """ permed_accounts = self.get_permed_accounts_for_user(user, asset) - accounts_mapper = {account.name: account for account in permed_accounts} + accounts_mapper = {account.alias: account for account in permed_accounts} account = accounts_mapper.get(account_name) return account @@ -44,21 +45,21 @@ class PermAccountUtil(AssetPermissionUtil): cleaned_accounts_expired = defaultdict(list) # @ALL 账号先处理,后面的每个最多映射一个账号 - all_action_bit = alias_action_bit_mapper.pop(Account.AliasAccount.ALL, None) + all_action_bit = alias_action_bit_mapper.pop(AliasAccount.ALL, None) if all_action_bit: for account in asset_accounts: cleaned_accounts_action_bit[account] |= all_action_bit cleaned_accounts_expired[account].extend( - alias_expired_mapper[Account.AliasAccount.ALL] + alias_expired_mapper[AliasAccount.ALL] ) for alias, action_bit in alias_action_bit_mapper.items(): - if alias == Account.AliasAccount.USER: + if alias == AliasAccount.USER: if user.username in username_account_mapper: account = username_account_mapper[user.username] else: account = Account.get_user_account(user.username) - elif alias == Account.AliasAccount.INPUT: + elif alias == AliasAccount.INPUT: account = Account.get_manual_account() elif alias in username_account_mapper: account = username_account_mapper[alias] diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py index 8e4cd9199..bbec44343 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -1,9 +1,14 @@ +from django.db.models import QuerySet +from assets.models import Node, Asset from common.utils import get_logger + from perms.models import AssetPermission logger = get_logger(__file__) +__all__ = ['AssetPermissionUtil'] + class AssetPermissionUtil(object): """ 资产授权相关的方法工具 """ @@ -31,22 +36,24 @@ class AssetPermissionUtil(object): group_ids = [g.id for g in user_groups] else: group_ids = user_groups.values_list('id', flat=True).distinct() - group_perm_ids = AssetPermission.user_groups.through.objects \ + perm_ids = AssetPermission.user_groups.through.objects \ .filter(usergroup_id__in=group_ids) \ .values_list('assetpermission_id', flat=True).distinct() if flat: - return group_perm_ids - perms = self.get_permissions(ids=group_perm_ids) + return perm_ids + perms = self.get_permissions(ids=perm_ids) return perms - def get_permissions_for_asset(self, asset, with_node=True, flat=False): + def get_permissions_for_assets(self, assets, with_node=True, flat=False): """ 获取资产的授权规则""" perm_ids = set() - asset_perm_ids = AssetPermission.assets.through.objects.filter(asset_id=asset.id) \ - .values_list('assetpermission_id', flat=True).distinct() + assets = self.convert_to_queryset_if_need(assets, Asset) + asset_ids = [str(a.id) for a in assets] + relations = AssetPermission.assets.through.objects.filter(asset_id__in=asset_ids) + asset_perm_ids = relations.values_list('assetpermission_id', flat=True).distinct() perm_ids.update(asset_perm_ids) if with_node: - nodes = asset.get_all_nodes() + nodes = Asset.get_all_nodes_for_assets(assets) node_perm_ids = self.get_permissions_for_nodes(nodes, flat=True) perm_ids.update(node_perm_ids) if flat: @@ -56,16 +63,12 @@ class AssetPermissionUtil(object): def get_permissions_for_nodes(self, nodes, with_ancestor=False, flat=False): """ 获取节点的授权规则 """ + nodes = self.convert_to_queryset_if_need(nodes, Node) if with_ancestor: - node_ids = set() - for node in nodes: - _nodes = node.get_ancestors(with_self=True) - _node_ids = _nodes.values_list('id', flat=True).distinct() - node_ids.update(_node_ids) - else: - node_ids = nodes.values_list('id', flat=True).distinct() - perm_ids = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) \ - .values_list('assetpermission_id', flat=True).distinct() + nodes = Node.get_ancestor_queryset(nodes) + node_ids = nodes.values_list('id', flat=True).distinct() + relations = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) + perm_ids = relations.values_list('assetpermission_id', flat=True).distinct() if flat: return perm_ids perms = self.get_permissions(ids=perm_ids) @@ -74,18 +77,30 @@ class AssetPermissionUtil(object): def get_permissions_for_user_asset(self, user, asset): """ 获取同时包含用户、资产的授权规则 """ user_perm_ids = self.get_permissions_for_user(user, flat=True) - asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) + asset_perm_ids = self.get_permissions_for_assets([asset], flat=True) perm_ids = set(user_perm_ids) & set(asset_perm_ids) perms = self.get_permissions(ids=perm_ids) return perms def get_permissions_for_user_group_asset(self, user_group, asset): user_perm_ids = self.get_permissions_for_user_groups([user_group], flat=True) - asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) + asset_perm_ids = self.get_permissions_for_assets([asset], flat=True) perm_ids = set(user_perm_ids) & set(asset_perm_ids) perms = self.get_permissions(ids=perm_ids) return perms + @staticmethod + def convert_to_queryset_if_need(objs_or_ids, model): + if not objs_or_ids: + return objs_or_ids + if isinstance(objs_or_ids, QuerySet) and isinstance(objs_or_ids.first(), model): + return objs_or_ids + ids = [ + str(i.id) if isinstance(i, model) else i + for i in objs_or_ids + ] + return model.objects.filter(id__in=ids) + @staticmethod def get_permissions(ids): perms = AssetPermission.objects.filter(id__in=ids).order_by('-date_expired') diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py new file mode 100644 index 000000000..ceea59b22 --- /dev/null +++ b/apps/perms/utils/user_perm.py @@ -0,0 +1,228 @@ +from assets.models import FavoriteAsset, Asset + +from django.conf import settings +from django.db.models import Q + +from common.utils.common import timeit + +from perms.models import AssetPermission, PermNode, UserAssetGrantedTreeNodeRelation + +from .permission import AssetPermissionUtil + + +__all__ = ['AssetPermissionPermAssetUtil', 'UserPermAssetUtil', 'UserPermNodeUtil'] + + +class AssetPermissionPermAssetUtil: + + def __init__(self, perm_ids): + self.perm_ids = perm_ids + + def get_all_assets(self): + """ 获取所有授权的资产 """ + node_asset_ids = self.get_perm_nodes_assets(flat=True) + direct_asset_ids = self.get_direct_assets(flat=True) + asset_ids = list(node_asset_ids) + list(direct_asset_ids) + assets = Asset.objects.filter(id__in=asset_ids) + return assets + + def get_perm_nodes_assets(self, flat=False): + """ 获取所有授权节点下的资产 """ + node_ids = AssetPermission.nodes.through.objects \ + .filter(assetpermission_id__in=self.perm_ids) \ + .values_list('node_id', flat=True) \ + .distinct() + node_ids = list(node_ids) + nodes = PermNode.objects.filter(id__in=node_ids).only('id', 'key') + assets = PermNode.get_nodes_all_assets(*nodes) + if flat: + return assets.values_list('id', flat=True) + return assets + + def get_direct_assets(self, flat=False): + """ 获取直接授权的资产 """ + assets = Asset.objects.order_by() \ + .filter(granted_by_permissions__id__in=self.perm_ids) \ + .distinct() + if flat: + return assets.values_list('id', flat=True) + return assets + + +class UserPermAssetUtil(AssetPermissionPermAssetUtil): + + def __init__(self, user): + self.user = user + perm_ids = AssetPermissionUtil().get_permissions_for_user(self.user, flat=True) + super().__init__(perm_ids) + + def get_ungroup_assets(self): + return self.get_direct_assets() + + def get_favorite_assets(self): + assets = self.get_all_assets() + asset_ids = FavoriteAsset.objects.filter(user=self.user).values_list('asset_id', flat=True) + assets = assets.filter(id__in=list(asset_ids)) + return assets + + def get_node_assets(self, key): + node = PermNode.objects.get(key=key) + node.compute_node_from_and_assets_amount(self.user) + if node.node_from == node.NodeFrom.granted: + assets = Asset.objects.filter(nodes__id=node.id).order_by() + elif node.node_from == node.NodeFrom.asset: + assets = self._get_indirect_perm_node_assets(node) + else: + assets = Asset.objects.none() + assets = assets.order_by('name') + return assets + + def get_node_all_assets(self, node_id): + """ 获取节点下的所有资产 """ + node = PermNode.objects.get(id=node_id) + node.compute_node_from_and_assets_amount(self.user) + if node.node_from == node.NodeFrom.granted: + assets = PermNode.get_nodes_all_assets() + elif node.node_from in (node.NodeFrom.asset, node.NodeFrom.child): + node.assets_amount = node.granted_assets_amount + assets = self._get_indirect_perm_node_all_assets(node) + else: + node.assets_amount = 0 + assets = Asset.objects.none() + return node, assets + + def _get_indirect_perm_node_assets(self, node): + """ 获取间接授权节点下的直接资产 """ + assets = self.get_direct_assets() + assets = assets.filter(nodes__id=node.id).order_by().distinct() + return assets + + def _get_indirect_perm_node_all_assets(self, node): + """ 获取间接授权节点下的所有资产 + 此算法依据 `UserAssetGrantedTreeNodeRelation` 的数据查询 + 1. 查询该节点下的直接授权节点 + 2. 查询该节点下授权资产关联的节点 + """ + # 查询节点下直接授权的子节点 + asset_ids = set() + children_from_granted = UserAssetGrantedTreeNodeRelation.objects \ + .filter(user=self.user) \ + .filter(node_key__startswith=f'{node.key}:', node_from=node.NodeFrom.granted) \ + .only('node_id', 'node_key') + for n in children_from_granted: + n.id = n.node_id + _assets = PermNode.get_nodes_all_assets(*children_from_granted) + _asset_ids = _assets.values_list('id', flat=True) + asset_ids.update(list(_asset_ids)) + + # 查询节点下资产授权的节点 + children_from_assets = UserAssetGrantedTreeNodeRelation.objects \ + .filter(user=self.user) \ + .filter(node_key__startswith=f'{node.key}:', node_from=node.NodeFrom.asset) \ + .values_list('node_id', flat=True) + children_from_assets = set(children_from_assets) + if node.node_from == node.NodeFrom.asset: + children_from_assets.add(node.id) + _asset_ids = Asset.objects \ + .filter(nodes__id__in=children_from_assets) \ + .filter(granted_by_permissions__id__in=self.perm_ids) \ + .distinct() \ + .order_by() \ + .values_list('id', flat=True) + asset_ids.update(list(_asset_ids)) + + return Asset.objects.filter(id__in=asset_ids) + + +class UserPermNodeUtil: + + def __init__(self, user): + self.user = user + self.perm_ids = AssetPermissionUtil().get_permissions_for_user(self.user, flat=True) + + def get_favorite_node(self): + assets_amount = UserPermAssetUtil(self.user).get_favorite_assets().count() + return PermNode.get_favorite_node(assets_amount) + + def get_ungrouped_node(self): + assets_amount = UserPermAssetUtil(self.user).get_direct_assets().count() + return PermNode.get_favorite_node(assets_amount) + + def get_top_level_nodes(self, with_unfolded_node=False): + # 是否有节点展开, 展开的节点 + unfolded_node = None + nodes = self.get_special_nodes() + real_nodes = self._get_perm_node_children_from_relation(key='') + nodes.extend(real_nodes) + if len(real_nodes) == 1: + unfolded_node = real_nodes[0] + children = self.get_node_children(unfolded_node.key) + nodes.extend(children) + if with_unfolded_node: + return nodes, unfolded_node + else: + return nodes + + def get_special_nodes(self): + nodes = [] + if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + ung_node = self.get_ungrouped_node() + nodes.append(ung_node) + fav_node = self.get_favorite_node() + nodes.append(fav_node) + return nodes + + def get_node_children(self, key): + if not key: + return self.get_top_level_nodes() + + if key in [PermNode.FAVORITE_NODE_KEY, PermNode.UNGROUPED_NODE_KEY]: + return PermNode.objects.none() + + node = PermNode.objects.get(key=key) + node.compute_node_from_and_assets_amount(self.user) + if node.node_from == node.NodeFrom.granted: + """ 直接授权的节点, 直接从完整资产树获取子节点 """ + children = PermNode.objects.filter(parent_key=key) + elif node.node_from in (node.NodeFrom.asset, node.NodeFrom.child): + """ 间接授权的节点, 从 Relation 表中获取子节点 """ + children = self._get_perm_node_children_from_relation(key) + else: + children = PermNode.objects.none() + children = sorted(children, key=lambda x: x.value) + return children + + def _get_perm_node_children_from_relation(self, key): + """ 获取授权节点的子节点, 从用户授权节点关系表中获取 """ + children = PermNode.objects.filter(granted_node_rels__user=self.user, parent_key=key) + children = children.annotate(**PermNode.annotate_granted_node_rel_fields).distinct() + for node in children: + node.assets_amount = node.granted_assets_amount + return children + + @timeit + def get_whole_tree_nodes(self, with_special=True): + user_nodes = PermNode.objects.filter(granted_node_rels__user=self.user) + user_nodes = user_nodes.annotate(**PermNode.annotate_granted_node_rel_fields).distinct() + + key_node_mapper = {} + q_nodes_descendant = Q() + for node in user_nodes: + node.assets_amount = node.granted_assets_amount + key_node_mapper[node.key] = node + if node.node_from == node.NodeFrom.granted: + """ 直接授权的节点, 增加后代节点的过滤条件 """ + q_nodes_descendant |= Q(key__startswith=f'{node.key}:') + if q_nodes_descendant: + descendant_nodes = PermNode.objects.filter(q_nodes_descendant) + for node in descendant_nodes: + key_node_mapper[node.key] = node + + nodes = [] + if with_special: + special_nodes = self.get_special_nodes() + nodes.extend(special_nodes) + nodes.extend(list(key_node_mapper.values())) + + return nodes + diff --git a/apps/perms/utils/user_perm_tree.py b/apps/perms/utils/user_perm_tree.py new file mode 100644 index 000000000..8888a67e9 --- /dev/null +++ b/apps/perms/utils/user_perm_tree.py @@ -0,0 +1,331 @@ +import time +from collections import defaultdict + +from django.conf import settings +from django.core.cache import cache + +from users.models import User +from assets.models import Asset +from assets.utils import NodeAssetsUtil +from orgs.models import Organization +from orgs.utils import ( + current_org, + tmp_to_org, + tmp_to_root_org +) +from common.decorator import on_transaction_commit +from common.utils import get_logger +from common.utils.common import lazyproperty, timeit +from common.db.models import output_as_string + +from perms.locks import UserGrantedTreeRebuildLock +from perms.models import ( + AssetPermission, + UserAssetGrantedTreeNodeRelation, + PermNode +) + +from .permission import AssetPermissionUtil + +logger = get_logger(__name__) + +__all__ = [ + 'UserPermTreeRefreshUtil', + 'UserPermTreeExpireUtil' +] + + +class _UserPermTreeCacheMixin: + """ 缓存数据 users: {org_id, org_id }, 记录用户授权树已经构建完成的组织集合 """ + cache_key_template = 'perms.user.node_tree.built_orgs.user_id:{user_id}' + + def get_cache_key(self, user_id): + return self.cache_key_template.format(user_id=user_id) + + @lazyproperty + def client(self): + return cache.client.get_client(write=True) + + +class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin): + """ 用户授权树刷新工具, 针对某一个用户授权树的刷新 """ + + def __init__(self, user): + self.user = user + self.orgs = self.user.orgs.distinct() + self.org_ids = [str(o.id) for o in self.orgs] + + @lazyproperty + def cache_key_user(self): + return self.get_cache_key(self.user.id) + + @timeit + def refresh_if_need(self, force=False): + self._clean_user_perm_tree_for_legacy_org() + to_refresh_orgs = self.orgs if force else self._get_user_need_refresh_orgs() + if not to_refresh_orgs: + logger.info('Not have to refresh orgs') + return + with UserGrantedTreeRebuildLock(self.user.id): + for org in to_refresh_orgs: + self._rebuild_user_perm_tree_for_org(org) + self._mark_user_orgs_refresh_finished(to_refresh_orgs) + + def _rebuild_user_perm_tree_for_org(self, org): + with tmp_to_org(org): + start = time.time() + UserPermTreeBuildUtil(self.user).rebuild_user_perm_tree() + end = time.time() + logger.info( + 'Refresh user [{user}] org [{org}] perm tree, user {use_time:.2f}s' + ''.format(user=self.user, org=org, use_time=end-start) + ) + + def _clean_user_perm_tree_for_legacy_org(self): + with tmp_to_root_org(): + """ Clean user legacy org node relations """ + user_relations = UserAssetGrantedTreeNodeRelation.objects.filter(user=self.user) + user_legacy_org_relations = user_relations.exclude(org_id__in=self.org_ids) + user_legacy_org_relations.delete() + + def _get_user_need_refresh_orgs(self): + cached_org_ids = self.client.smembers(self.cache_key_user) + cached_org_ids = {oid.decode() for oid in cached_org_ids} + to_refresh_org_ids = set(self.org_ids) - cached_org_ids + to_refresh_orgs = Organization.objects.filter(id__in=to_refresh_org_ids) + logger.info(f'Need to refresh orgs: {to_refresh_orgs}') + return to_refresh_orgs + + def _mark_user_orgs_refresh_finished(self, orgs): + org_ids = [str(org.id) for org in orgs] + self.client.sadd(self.cache_key_user, *org_ids) + + +class UserPermTreeExpireUtil(_UserPermTreeCacheMixin): + """ 用户授权树过期工具 """ + + @lazyproperty + def cache_key_all_user(self): + return self.get_cache_key('*') + + def expire_perm_tree_for_nodes_assets(self, node_ids, asset_ids): + node_perm_ids = AssetPermissionUtil().get_permissions_for_nodes(node_ids, flat=True) + asset_perm_ids = AssetPermissionUtil().get_permissions_for_assets(asset_ids, flat=True) + perm_ids = set(node_perm_ids) | set(asset_perm_ids) + self.expire_perm_tree_for_perms(perm_ids) + + @tmp_to_root_org() + def expire_perm_tree_for_perms(self, perm_ids): + org_perm_ids = AssetPermission.objects.filter(id__in=perm_ids).values_list('org_id', 'id') + org_perms_mapper = defaultdict(set) + for org_id, perm_id in org_perm_ids: + org_perms_mapper[org_id].add(perm_id) + for org_id, perms_id in org_perms_mapper.items(): + user_ids = AssetPermission.get_all_users_for_perms(perm_ids, flat=True) + self.expire_perm_tree_for_users_orgs(user_ids, [org_id]) + + def expire_perm_tree_for_user_group(self, user_group): + group_ids = [user_group.id] + org_ids = [user_group.org_id] + self.expire_perm_tree_for_user_groups_orgs(group_ids, org_ids) + + def expire_perm_tree_for_user_groups_orgs(self, group_ids, org_ids): + user_ids = User.groups.through.objects.filter(usergroup_id__in=group_ids) \ + .values_list('user_id', flat=True).distinct() + self.expire_perm_tree_for_users_orgs(user_ids, org_ids) + + @on_transaction_commit + def expire_perm_tree_for_users_orgs(self, user_ids, org_ids): + org_ids = [str(oid) for oid in org_ids] + with self.client.pipeline() as p: + for uid in user_ids: + cache_key = self.get_cache_key(uid) + p.srem(cache_key, *org_ids) + p.execute() + logger.info('Expire perm tree for users: [{}], orgs: [{}]'.format(user_ids, org_ids)) + + def expire_perm_tree_for_all_user(self): + keys = self.client.keys(self.cache_key_all_user) + with self.client.pipeline() as p: + for k in keys: + p.delete(k) + p.execute() + logger.info('Expire all user perm tree') + + +class UserPermTreeBuildUtil(object): + node_only_fields = ('id', 'key', 'parent_key', 'org_id') + + def __init__(self, user): + self.user = user + self.user_perm_ids = AssetPermissionUtil().get_permissions_for_user(self.user, flat=True) + # {key: node} + self._perm_nodes_key_node_mapper = {} + + def rebuild_user_perm_tree(self): + self.clean_user_perm_tree() + if not self.user_perm_ids: + logger.info('User({}) not have permissions'.format(self.user)) + return + self.compute_perm_nodes() + self.compute_perm_nodes_asset_amount() + self.create_mapping_nodes() + + def clean_user_perm_tree(self): + UserAssetGrantedTreeNodeRelation.objects.filter(user=self.user).delete() + + def compute_perm_nodes(self): + self._compute_perm_nodes_for_direct() + self._compute_perm_nodes_for_direct_asset_if_need() + self._compute_perm_nodes_for_ancestor() + + def compute_perm_nodes_asset_amount(self): + """ 这里计算的是一个组织的授权树 """ + computed = self._only_compute_root_node_assets_amount_if_need() + if computed: + return + + nodekey_assetid_mapper = defaultdict(set) + org_id = current_org.id + for key in self.perm_node_keys_for_granted: + asset_ids = PermNode.get_all_asset_ids_by_node_key(org_id, key) + nodekey_assetid_mapper[key].update(asset_ids) + + for asset_id, node_id in self.direct_asset_id_node_id_pairs: + node_key = self.perm_nodes_id_key_mapper.get(node_id) + if not node_key: + continue + nodekey_assetid_mapper[node_key].add(asset_id) + + util = NodeAssetsUtil(self.perm_nodes, nodekey_assetid_mapper) + util.generate() + + for node in self.perm_nodes: + assets_amount = util.get_assets_amount(node.key) + node.assets_amount = assets_amount + + def create_mapping_nodes(self): + to_create = [] + for node in self.perm_nodes: + relation = UserAssetGrantedTreeNodeRelation( + user=self.user, + node=node, + node_key=node.key, + node_parent_key=node.parent_key, + node_from=node.node_from, + node_assets_amount=node.assets_amount, + org_id=node.org_id + ) + to_create.append(relation) + + UserAssetGrantedTreeNodeRelation.objects.bulk_create(to_create) + + def _compute_perm_nodes_for_direct(self): + """ 直接授权的节点(叶子节点)""" + for node in self.direct_nodes: + if self.has_any_ancestor_direct_permed(node): + continue + node.node_from = node.NodeFrom.granted + self._perm_nodes_key_node_mapper[node.key] = node + + def _compute_perm_nodes_for_direct_asset_if_need(self): + """ 直接授权的资产所在的节点(叶子节点)""" + if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + return + for node in self.direct_asset_nodes: + if self.has_any_ancestor_direct_permed(node): + continue + if node.key in self._perm_nodes_key_node_mapper: + continue + node.node_from = node.NodeFrom.asset + self._perm_nodes_key_node_mapper[node.key] = node + + def _compute_perm_nodes_for_ancestor(self): + """ 直接授权节点 和 直接授权资产所在节点 的所有祖先节点 (构造完整树) """ + ancestor_keys = set() + for node in self._perm_nodes_key_node_mapper.values(): + ancestor_keys.update(node.get_ancestor_keys()) + ancestor_keys -= set(self._perm_nodes_key_node_mapper.keys()) + + ancestors = PermNode.objects.filter(key__in=ancestor_keys).only(*self.node_only_fields) + for node in ancestors: + node.node_from = node.NodeFrom.child + self._perm_nodes_key_node_mapper[node.key] = node + + @lazyproperty + def perm_node_keys_for_granted(self): + keys = [ + key for key, node in self._perm_nodes_key_node_mapper.items() + if node.node_from == node.NodeFrom.granted + ] + return keys + + @lazyproperty + def perm_nodes_id_key_mapper(self): + mapper = { + node.id.hex: node.key + for key, node in self._perm_nodes_key_node_mapper.items() + } + return mapper + + def _only_compute_root_node_assets_amount_if_need(self): + if len(self.perm_nodes) != 1: + return False + root_node = self.perm_nodes[0] + if not root_node.is_org_root(): + return False + if root_node.node_from != root_node.NodeFrom.granted: + return False + root_node.granted_assets_amount = len(root_node.get_all_asset_ids()) + return True + + @lazyproperty + def perm_nodes(self): + """ 授权树的所有节点 """ + return list(self._perm_nodes_key_node_mapper.values()) + + def has_any_ancestor_direct_permed(self, node): + """ 任何一个祖先节点被直接授权 """ + return bool(set(node.get_ancestor_keys()) & set(self.direct_node_keys)) + + @lazyproperty + def direct_node_keys(self): + """ 直接授权的节点 keys """ + return {n.key for n in self.direct_nodes} + + @lazyproperty + def direct_nodes(self): + """ 直接授权的节点 """ + node_ids = AssetPermission.nodes.through.objects \ + .filter(assetpermission_id__in=self.user_perm_ids) \ + .values_list('node_id', flat=True).distinct() + nodes = PermNode.objects.filter(id__in=node_ids).only(*self.node_only_fields) + return nodes + + @lazyproperty + def direct_asset_nodes(self): + """ 获取直接授权的资产所在的节点 """ + node_ids = [node_id for asset_id, node_id in self.direct_asset_id_node_id_pairs] + nodes = PermNode.objects.filter(id__in=node_ids).distinct().only(*self.node_only_fields) + return nodes + + @lazyproperty + def direct_asset_id_node_id_pairs(self): + """ 直接授权的资产 id 和 节点 id """ + asset_node_pairs = Asset.nodes.through.objects \ + .filter(asset_id__in=self.direct_asset_ids) \ + .annotate( + str_asset_id=output_as_string('asset_id'), + str_node_id=output_as_string('node_id') + ).values_list('str_asset_id', 'str_node_id') + asset_node_pairs = list(asset_node_pairs) + return asset_node_pairs + + @lazyproperty + def direct_asset_ids(self): + """ 直接授权的资产 ids """ + asset_ids = AssetPermission.assets.through.objects \ + .filter(assetpermission_id__in=self.user_perm_ids) \ + .values_list('asset_id', flat=True) \ + .distinct() + return asset_ids diff --git a/apps/perms/utils/user_permission.py b/apps/perms/utils/user_permission.py deleted file mode 100644 index 92a42a401..000000000 --- a/apps/perms/utils/user_permission.py +++ /dev/null @@ -1,685 +0,0 @@ -import time -from collections import defaultdict -from typing import List, Tuple - -from django.conf import settings -from django.core.cache import cache -from django.db.models import Q, QuerySet -from django.utils.translation import gettext as _ - -from assets.models import ( - Asset, FavoriteAsset, AssetQuerySet, NodeQuerySet -) -from assets.utils import NodeAssetsUtil -from common.db.models import output_as_string, UnionQuerySet -from common.decorator import on_transaction_commit -from common.utils import get_logger -from common.utils.common import lazyproperty, timeit -from orgs.models import Organization -from orgs.utils import ( - tmp_to_org, current_org, - ensure_in_real_or_default_org, tmp_to_root_org -) -from perms.locks import UserGrantedTreeRebuildLock -from perms.models import ( - AssetPermission, PermNode, UserAssetGrantedTreeNodeRelation -) -from users.models import User - -NodeFrom = UserAssetGrantedTreeNodeRelation.NodeFrom -NODE_ONLY_FIELDS = ('id', 'key', 'parent_key', 'org_id') - -logger = get_logger(__name__) - - -def get_user_all_asset_perm_ids(user) -> set: - asset_perm_ids = set() - user_perm_id = AssetPermission.users.through.objects \ - .filter(user_id=user.id) \ - .values_list('assetpermission_id', flat=True) \ - .distinct() - asset_perm_ids.update(user_perm_id) - - group_ids = user.groups.through.objects \ - .filter(user_id=user.id) \ - .values_list('usergroup_id', flat=True) \ - .distinct() - group_ids = list(group_ids) - groups_perm_id = AssetPermission.user_groups.through.objects \ - .filter(usergroup_id__in=group_ids) \ - .values_list('assetpermission_id', flat=True) \ - .distinct() - asset_perm_ids.update(groups_perm_id) - - asset_perm_ids = AssetPermission.objects.filter( - id__in=asset_perm_ids).valid().values_list('id', flat=True) - asset_perm_ids = set(asset_perm_ids) - return asset_perm_ids - - -class UserGrantedTreeRefreshController: - key_template = 'perms.user.node_tree.built_orgs.user_id:{user_id}' - - def __init__(self, user): - self.user = user - self.key = self.key_template.format(user_id=user.id) - self.client = self.get_redis_client() - - @classmethod - def clean_all_user_tree_built_mark(cls): - """ 清除所有用户已构建树的标记 """ - client = cls.get_redis_client() - key_match = cls.key_template.format(user_id='*') - keys = client.keys(key_match) - with client.pipeline() as p: - for key in keys: - p.delete(key) - p.execute() - - @classmethod - def get_redis_client(cls): - return cache.client.get_client(write=True) - - def get_need_refresh_org_ids(self): - org_ids = self.client.smembers(self.key) - return {org_id.decode() for org_id in org_ids} - - def set_all_orgs_as_built(self): - self.client.sadd(self.key, *self.org_ids) - - def have_need_refresh_orgs(self): - built_org_ids = self.client.smembers(self.key) - built_org_ids = {org_id.decode() for org_id in built_org_ids} - have = self.org_ids - built_org_ids - return have - - def get_need_refresh_orgs_and_fill_up(self): - org_ids = self.org_ids - - with self.client.pipeline() as p: - p.smembers(self.key) - p.sadd(self.key, *org_ids) - ret = p.execute() - built_org_ids = {org_id.decode() for org_id in ret[0]} - ids = org_ids - built_org_ids - orgs = {*Organization.objects.filter(id__in=ids)} - logger.info( - f'Need rebuild orgs are {orgs}, built orgs are {ret[0]}, ' - f'all orgs are {org_ids}' - ) - return orgs - - @classmethod - @on_transaction_commit - def remove_built_orgs_from_users(cls, org_ids, user_ids): - client = cls.get_redis_client() - org_ids = [str(org_id) for org_id in org_ids] - - with client.pipeline() as p: - for user_id in user_ids: - key = cls.key_template.format(user_id=user_id) - p.srem(key, *org_ids) - p.execute() - logger.info(f'Remove orgs from users built tree: users:{user_ids} orgs:{org_ids}') - - @classmethod - def add_need_refresh_orgs_for_users(cls, org_ids, user_ids): - cls.remove_built_orgs_from_users(org_ids, user_ids) - - @classmethod - @ensure_in_real_or_default_org - def add_need_refresh_on_nodes_assets_relate_change(cls, node_ids, asset_ids): - """ - 1,计算与这些资产有关的授权 - 2,计算与这些节点以及祖先节点有关的授权 - """ - - node_ids = set(node_ids) - ancestor_node_keys = set() - asset_perm_ids = set() - - nodes = PermNode.objects.filter(id__in=node_ids).only('id', 'key') - for node in nodes: - ancestor_node_keys.update(node.get_ancestor_keys()) - - ancestor_id = PermNode.objects.filter(key__in=ancestor_node_keys).values_list('id', flat=True) - node_ids.update(ancestor_id) - - assets_related_perm_ids = AssetPermission.nodes.through.objects.filter( - node_id__in=node_ids - ).values_list('assetpermission_id', flat=True) - asset_perm_ids.update(assets_related_perm_ids) - - nodes_related_perm_ids = AssetPermission.assets.through.objects.filter( - asset_id__in=asset_ids - ).values_list('assetpermission_id', flat=True) - asset_perm_ids.update(nodes_related_perm_ids) - - cls.add_need_refresh_by_asset_perm_ids(asset_perm_ids) - - @classmethod - def add_need_refresh_by_asset_perm_ids_cross_orgs(cls, asset_perm_ids): - org_id_perm_ids_mapper = defaultdict(set) - pairs = AssetPermission.objects.filter(id__in=asset_perm_ids).values_list('org_id', 'id') - for org_id, perm_id in pairs: - org_id_perm_ids_mapper[org_id].add(perm_id) - for org_id, perm_ids in org_id_perm_ids_mapper.items(): - with tmp_to_org(org_id): - cls.add_need_refresh_by_asset_perm_ids(perm_ids) - - @classmethod - @ensure_in_real_or_default_org - def add_need_refresh_by_asset_perm_ids(cls, asset_perm_ids): - - group_ids = AssetPermission.user_groups.through.objects.filter( - assetpermission_id__in=asset_perm_ids - ).values_list('usergroup_id', flat=True) - - user_ids = set() - direct_user_id = AssetPermission.users.through.objects.filter( - assetpermission_id__in=asset_perm_ids - ).values_list('user_id', flat=True) - user_ids.update(direct_user_id) - - group_user_ids = User.groups.through.objects.filter( - usergroup_id__in=group_ids - ).values_list('user_id', flat=True) - user_ids.update(group_user_ids) - - cls.remove_built_orgs_from_users( - [current_org.id], user_ids - ) - - @lazyproperty - def org_ids(self): - ret = {str(org.id) for org in self.orgs} - return ret - - @lazyproperty - def orgs(self): - orgs = {*self.user.orgs.all().distinct()} - return orgs - - @timeit - def refresh_if_need(self, force=False): - user = self.user - - with tmp_to_root_org(): - UserAssetGrantedTreeNodeRelation.objects.filter(user=user) \ - .exclude(org_id__in=self.org_ids) \ - .delete() - - if not force and not self.have_need_refresh_orgs(): - return - - with UserGrantedTreeRebuildLock(user_id=user.id): - if force: - orgs = self.orgs - self.set_all_orgs_as_built() - else: - orgs = self.get_need_refresh_orgs_and_fill_up() - - for org in orgs: - with tmp_to_org(org): - t_start = time.time() - logger.info(f'Rebuild user tree: user={self.user} org={current_org}') - utils = UserGrantedTreeBuildUtils(user) - utils.rebuild_user_granted_tree() - logger.info( - f'Rebuild user tree ok: cost={time.time() - t_start} ' - f'user={self.user} org={current_org}' - ) - - -class UserGrantedUtilsBase: - user: User - - def __init__(self, user, asset_perm_ids=None): - self.user = user - self._asset_perm_ids = asset_perm_ids and set(asset_perm_ids) - - @lazyproperty - def asset_perm_ids(self) -> set: - if self._asset_perm_ids: - return self._asset_perm_ids - - asset_perm_ids = get_user_all_asset_perm_ids(self.user) - return asset_perm_ids - - -class UserGrantedTreeBuildUtils(UserGrantedUtilsBase): - - def get_direct_granted_nodes(self) -> NodeQuerySet: - # 查询直接授权节点 - nodes = PermNode.objects.filter( - granted_by_permissions__id__in=self.asset_perm_ids - ).distinct() - return nodes - - @lazyproperty - def direct_granted_asset_ids(self) -> list: - # 3.15 - asset_ids = AssetPermission.assets.through.objects.filter( - assetpermission_id__in=self.asset_perm_ids - ).annotate( - asset_id_str=output_as_string('asset_id') - ).values_list( - 'asset_id_str', flat=True - ).distinct() - - asset_ids = list(asset_ids) - return asset_ids - - @ensure_in_real_or_default_org - def rebuild_user_granted_tree(self): - """ - 注意:调用该方法一定要被 `UserGrantedTreeRebuildLock` 锁住 - """ - user = self.user - - # 先删除旧的授权树🌲 - UserAssetGrantedTreeNodeRelation.objects.filter(user=user).delete() - - if not self.asset_perm_ids: - # 没有授权直接返回 - return - - nodes = self.compute_perm_nodes_tree() - self.compute_node_assets_amount(nodes) - if not nodes: - return - self.create_mapping_nodes(nodes) - - @timeit - def compute_perm_nodes_tree(self, node_only_fields=NODE_ONLY_FIELDS) -> list: - - # 查询直接授权节点 - nodes = self.get_direct_granted_nodes().only(*node_only_fields) - nodes = list(nodes) - - # 授权的节点 key 集合 - granted_key_set = {_node.key for _node in nodes} - - def _has_ancestor_granted(node: PermNode): - """ - 判断一个节点是否有授权过的祖先节点 - """ - ancestor_keys = set(node.get_ancestor_keys()) - return ancestor_keys & granted_key_set - - key2leaf_nodes_mapper = {} - - # 给授权节点设置 granted 标识,同时去重 - for node in nodes: - node: PermNode - if _has_ancestor_granted(node): - continue - node.node_from = NodeFrom.granted - key2leaf_nodes_mapper[node.key] = node - - # 查询授权资产关联的节点设置 - def process_direct_granted_assets(): - # 查询直接授权资产 - node_ids = {node_id_str for node_id_str, _ in self.direct_granted_asset_id_node_id_str_pairs} - # 查询授权资产关联的节点设置 2.80 - granted_asset_nodes = PermNode.objects.filter( - id__in=node_ids - ).distinct().only(*node_only_fields) - granted_asset_nodes = list(granted_asset_nodes) - - # 给资产授权关联的节点设置 is_asset_granted 标识,同时去重 - for node in granted_asset_nodes: - if _has_ancestor_granted(node): - continue - if node.key in key2leaf_nodes_mapper: - continue - node.node_from = NodeFrom.asset - key2leaf_nodes_mapper[node.key] = node - - if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - process_direct_granted_assets() - - leaf_nodes = key2leaf_nodes_mapper.values() - - # 计算所有祖先节点 - ancestor_keys = set() - for node in leaf_nodes: - ancestor_keys.update(node.get_ancestor_keys()) - - # 从祖先节点 key 中去掉同时也是叶子节点的 key - ancestor_keys -= key2leaf_nodes_mapper.keys() - # 查出祖先节点 - ancestors = PermNode.objects.filter(key__in=ancestor_keys).only(*node_only_fields) - ancestors = list(ancestors) - for node in ancestors: - node.node_from = NodeFrom.child - result = [*leaf_nodes, *ancestors] - return result - - @timeit - def create_mapping_nodes(self, nodes): - user = self.user - to_create = [] - - for node in nodes: - to_create.append(UserAssetGrantedTreeNodeRelation( - user=user, - node=node, - node_key=node.key, - node_parent_key=node.parent_key, - node_from=node.node_from, - node_assets_amount=node.assets_amount, - org_id=node.org_id - )) - - UserAssetGrantedTreeNodeRelation.objects.bulk_create(to_create) - - @timeit - def _fill_direct_granted_node_asset_ids_from_mem(self, nodes_key, mapper): - org_id = current_org.id - for key in nodes_key: - asset_ids = PermNode.get_all_asset_ids_by_node_key(org_id, key) - mapper[key].update(asset_ids) - - @lazyproperty - def direct_granted_asset_id_node_id_str_pairs(self): - node_asset_pairs = Asset.nodes.through.objects.filter( - asset_id__in=self.direct_granted_asset_ids - ).annotate( - asset_id_str=output_as_string('asset_id'), - node_id_str=output_as_string('node_id') - ).values_list( - 'node_id_str', 'asset_id_str' - ) - node_asset_pairs = list(node_asset_pairs) - return node_asset_pairs - - @timeit - def compute_node_assets_amount(self, nodes: List[PermNode]): - """ - 这里计算的是一个组织的 - """ - # 直接授权了根节点,直接计算 - if len(nodes) == 1: - node = nodes[0] - if node.node_from == NodeFrom.granted and node.key.isdigit(): - with tmp_to_org(node.org): - node.granted_assets_amount = len(node.get_all_asset_ids()) - return - - direct_granted_nodes_key = [] - node_id_key_mapper = {} - for node in nodes: - if node.node_from == NodeFrom.granted: - direct_granted_nodes_key.append(node.key) - node_id_key_mapper[node.id.hex] = node.key - - # 授权的节点和直接资产的映射 - nodekey_assetsid_mapper = defaultdict(set) - # 直接授权的节点,资产从完整树过来 - self._fill_direct_granted_node_asset_ids_from_mem( - direct_granted_nodes_key, nodekey_assetsid_mapper - ) - - # 处理直接授权资产 - # 直接授权资产,取节点与资产的关系 - node_asset_pairs = self.direct_granted_asset_id_node_id_str_pairs - node_asset_pairs = list(node_asset_pairs) - - for node_id, asset_id in node_asset_pairs: - if node_id not in node_id_key_mapper: - continue - node_key = node_id_key_mapper[node_id] - nodekey_assetsid_mapper[node_key].add(asset_id) - - util = NodeAssetsUtil(nodes, nodekey_assetsid_mapper) - util.generate() - - for node in nodes: - assets_amount = util.get_assets_amount(node.key) - node.assets_amount = assets_amount - - def get_whole_tree_nodes(self) -> list: - node_only_fields = NODE_ONLY_FIELDS + ('value', 'full_value') - nodes = self.compute_perm_nodes_tree(node_only_fields=node_only_fields) - self.compute_node_assets_amount(nodes) - - # 查询直接授权节点的子节点 - q = Q() - for node in self.get_direct_granted_nodes().only('key'): - q |= Q(key__startswith=f'{node.key}:') - - if q: - descendant_nodes = PermNode.objects.filter(q).distinct() - else: - descendant_nodes = PermNode.objects.none() - - nodes.extend(descendant_nodes) - return nodes - - -class UserGrantedAssetsQueryUtils(UserGrantedUtilsBase): - - def get_favorite_assets(self) -> QuerySet: - favorite_asset_ids = FavoriteAsset.objects.filter( - user=self.user - ).values_list('asset_id', flat=True) - favorite_asset_ids = list(favorite_asset_ids) - assets = self.get_all_granted_assets() - assets = assets.filter(id__in=favorite_asset_ids) - return assets - - def get_ungroup_assets(self) -> AssetQuerySet: - return self.get_direct_granted_assets() - - def get_direct_granted_assets(self) -> AssetQuerySet: - queryset = Asset.objects.order_by().filter( - granted_by_permissions__id__in=self.asset_perm_ids - ).distinct() - return queryset - - def get_direct_granted_nodes_assets(self) -> AssetQuerySet: - granted_node_ids = AssetPermission.nodes.through.objects.filter( - assetpermission_id__in=self.asset_perm_ids - ).values_list('node_id', flat=True).distinct() - granted_node_ids = list(granted_node_ids) - granted_nodes = PermNode.objects.filter(id__in=granted_node_ids).only('id', 'key') - queryset = PermNode.get_nodes_all_assets(*granted_nodes) - return queryset - - def get_all_granted_assets(self) -> QuerySet: - nodes_assets = self.get_direct_granted_nodes_assets() - assets = self.get_direct_granted_assets() - queryset = UnionQuerySet(nodes_assets, assets) - return queryset - - def get_node_all_assets(self, id) -> Tuple[PermNode, QuerySet]: - node = PermNode.objects.get(id=id) - granted_status = node.get_granted_status(self.user) - if granted_status == NodeFrom.granted: - assets = PermNode.get_nodes_all_assets(node) - return node, assets - elif granted_status in (NodeFrom.asset, NodeFrom.child): - node.use_granted_assets_amount() - assets = self._get_indirect_granted_node_all_assets(node) - return node, assets - else: - node.assets_amount = 0 - return node, Asset.objects.none() - - def get_node_assets(self, key) -> AssetQuerySet: - node = PermNode.objects.get(key=key) - granted_status = node.get_granted_status(self.user) - - if granted_status == NodeFrom.granted: - assets = Asset.objects.order_by().filter(nodes__id=node.id) - elif granted_status == NodeFrom.asset: - assets = self._get_indirect_granted_node_assets(node.id) - else: - assets = Asset.objects.none() - assets = assets.order_by('name') - return assets - - def _get_indirect_granted_node_assets(self, id) -> AssetQuerySet: - assets = Asset.objects.order_by().filter(nodes__id=id).distinct() & self.get_direct_granted_assets() - return assets - - def _get_indirect_granted_node_all_assets(self, node) -> QuerySet: - """ - 此算法依据 `UserAssetGrantedTreeNodeRelation` 的数据查询 - 1. 查询该节点下的直接授权节点 - 2. 查询该节点下授权资产关联的节点 - """ - user = self.user - - # 查询该节点下的授权节点 - granted_nodes = UserAssetGrantedTreeNodeRelation.objects.filter( - user=user, node_from=NodeFrom.granted - ).filter( - Q(node_key__startswith=f'{node.key}:') - ).only('node_id', 'node_key') - - for n in granted_nodes: - n.id = n.node_id - - node_assets = PermNode.get_nodes_all_assets(*granted_nodes) - - # 查询该节点下的资产授权节点 - only_asset_granted_node_ids = UserAssetGrantedTreeNodeRelation.objects.filter( - user=user, node_from=NodeFrom.asset - ).filter( - Q(node_key__startswith=f'{node.key}:') - ).values_list('node_id', flat=True) - - only_asset_granted_node_ids = list(only_asset_granted_node_ids) - if node.node_from == NodeFrom.asset: - only_asset_granted_node_ids.append(node.id) - - assets = Asset.objects.filter( - nodes__id__in=only_asset_granted_node_ids, - granted_by_permissions__id__in=self.asset_perm_ids - ).distinct().order_by() - granted_assets = UnionQuerySet(node_assets, assets) - return granted_assets - - -class UserGrantedNodesQueryUtils(UserGrantedUtilsBase): - def sort(self, nodes): - nodes = sorted(nodes, key=lambda x: x.value) - return nodes - - def get_node_children(self, key): - if not key: - return self.get_top_level_nodes() - - nodes = PermNode.objects.none() - if key in [PermNode.FAVORITE_NODE_KEY, PermNode.UNGROUPED_NODE_KEY]: - return nodes - - node = PermNode.objects.get(key=key) - granted_status = node.get_granted_status(self.user) - if granted_status == NodeFrom.granted: - nodes = PermNode.objects.filter(parent_key=key) - elif granted_status in (NodeFrom.asset, NodeFrom.child): - nodes = self.get_indirect_granted_node_children(key) - nodes = self.sort(nodes) - return nodes - - def get_indirect_granted_node_children(self, key): - """ - 获取用户授权树中未授权节点的子节点 - 只匹配在 `UserAssetGrantedTreeNodeRelation` 中存在的节点 - """ - user = self.user - nodes = PermNode.objects.filter( - granted_node_rels__user=user, - parent_key=key - ).annotate( - **PermNode.annotate_granted_node_rel_fields - ).distinct() - - # 设置节点授权资产数量 - for node in nodes: - node.use_granted_assets_amount() - return nodes - - def get_top_level_nodes(self): - nodes = self.get_special_nodes() - real_nodes = self.get_indirect_granted_node_children('') - nodes.extend(real_nodes) - if len(real_nodes) == 1: - children = self.get_node_children(real_nodes[0].key) - nodes.extend(children) - return nodes - - def get_ungrouped_node(self): - assets_util = UserGrantedAssetsQueryUtils(self.user, self.asset_perm_ids) - assets_amount = assets_util.get_direct_granted_assets().count() - return PermNode.get_ungrouped_node(assets_amount) - - def get_favorite_node(self): - assets_query_utils = UserGrantedAssetsQueryUtils(self.user, self.asset_perm_ids) - assets_amount = assets_query_utils.get_favorite_assets().values_list('id').count() - return PermNode.get_favorite_node(assets_amount) - - @staticmethod - def get_root_node(): - name = _('My assets') - node = { - 'id': '', - 'name': name, - 'title': name, - 'pId': '', - 'open': True, - 'isParent': True, - 'meta': { - 'type': 'root' - } - } - return node - - def get_special_nodes(self): - nodes = [] - if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - ungrouped_node = self.get_ungrouped_node() - nodes.append(ungrouped_node) - favorite_node = self.get_favorite_node() - nodes.append(favorite_node) - return nodes - - @timeit - def get_whole_tree_nodes(self, with_special=True): - """ - 这里的 granted nodes, 是整棵树需要的node,推算出来的也算 - :param with_special: - :return: - """ - nodes = PermNode.objects.filter(granted_node_rels__user=self.user) \ - .annotate(**PermNode.annotate_granted_node_rel_fields) \ - .distinct() - - key_to_node_mapper = {} - nodes_descendant_q = Q() - - for node in nodes: - node.use_granted_assets_amount() - key_to_node_mapper[node.key] = node - - if node.node_from == NodeFrom.granted: - # 直接授权的节点 - # 增加查询后代节点的过滤条件 - nodes_descendant_q |= Q(key__startswith=f'{node.key}:') - - if nodes_descendant_q: - descendant_nodes = PermNode.objects.filter( - nodes_descendant_q - ) - for node in descendant_nodes: - key_to_node_mapper[node.key] = node - - all_nodes = [] - if with_special: - special_nodes = self.get_special_nodes() - all_nodes.extend(special_nodes) - all_nodes.extend(key_to_node_mapper.values()) - return all_nodes diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 541b0b3da..b81ec1bf9 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -27,10 +27,6 @@ exclude_permissions = ( ('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'), ('authentication', 'temptoken', 'delete', 'temptoken'), ('users', 'userpasswordhistory', '*', '*'), - ('applications', 'applicationuser', '*', '*'), - ('applications', 'historicalaccount', '*', '*'), - ('applications', 'account', 'add,change,delete', 'account'), - ('applications', 'account', 'change', 'appplicationaccountsecret'), ('assets', 'adminuser', '*', '*'), ('assets', 'assetgroup', '*', '*'), ('assets', 'cluster', '*', '*'), @@ -39,6 +35,25 @@ exclude_permissions = ( ('assets', 'assetuser', '*', '*'), ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), ('assets', 'accountbackupplanexecution', 'delete,change', 'accountbackupplanexecution'), + ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), + ('assets', 'web', '*', '*'), + ('assets', 'host', '*', '*'), + ('assets', 'cloud', '*', '*'), + ('assets', 'device', '*', '*'), + ('assets', 'database', '*', '*'), + ('assets', 'protocol', '*', '*'), + ('assets', 'systemuser', '*', '*'), + ('assets', 'baseautomation', '*', '*'), + ('assets', 'pingautomation', '*', '*'), + ('assets', 'platformprotocol', '*', '*'), + ('assets', 'platformautomation', '*', '*'), + ('assets', 'gatherfactsautomation', '*', '*'), + ('assets', 'pushaccountautomation', '*', '*'), + ('assets', 'verifyaccountautomation', '*', '*'), + ('assets', 'changesecretrecord', 'add,delete,change', 'changesecretrecord'), + ('assets', 'automationexecution', '*', 'automationexecution'), + ('assets', 'commandfilter', '*', '*'), + ('assets', 'commandfilterrule', '*', '*'), # TODO 暂时去掉历史账号的权限 ('assets', 'account', '*', 'assethistoryaccount'), ('assets', 'account', '*', 'assethistoryaccountsecret'), @@ -57,7 +72,10 @@ exclude_permissions = ( ('rbac', 'role', '*', '*'), ('ops', 'adhoc', 'delete,change', '*'), ('ops', 'adhocexecution', 'add,delete,change', '*'), - ('ops', 'task', 'add,change', 'task'), + ('ops', 'jobexecution', 'change,delete', 'jobexecution'), + ('ops', 'historicaljob', '*', '*'), + ('ops', 'celerytask', 'add,change,delete', 'celerytask'), + ('ops', 'celerytaskexecution', 'add,change,delete', 'celerytaskexecution'), ('ops', 'commandexecution', 'delete,change', 'commandexecution'), ('orgs', 'organizationmember', '*', '*'), ('settings', 'setting', 'add,change,delete', 'setting'), @@ -82,9 +100,6 @@ exclude_permissions = ( ('xpack', 'license', '*', '*'), ('xpack', 'syncinstancedetail', 'add,delete,change', 'syncinstancedetail'), ('xpack', 'syncinstancetaskexecution', 'delete,change', 'syncinstancetaskexecution'), - ('xpack', 'changeauthplanexecution', 'delete,change', 'changeauthplanexecution'), - ('xpack', 'changeauthplantask', 'add,delete', 'changeauthplantask'), - ('xpack', 'gatherusertaskexecution', 'change,delete', 'gatherusertaskexecution'), ('common', 'permission', 'add,delete,view,change', 'permission'), ('terminal', 'command', 'delete,change', 'command'), ('terminal', 'status', 'delete,change', 'status'), @@ -94,6 +109,9 @@ exclude_permissions = ( ('terminal', 'sessionsharing', 'view,add,change,delete', 'sessionsharing'), ('terminal', 'session', 'delete,share', 'session'), ('terminal', 'session', 'delete,change', 'command'), + ('terminal', 'appletpublication', '*', '*'), + ('terminal', 'applethostdeployment', '*', '*'), + ('applications', '*', '*', '*'), ) diff --git a/apps/rbac/migrations/0001_initial.py b/apps/rbac/migrations/0001_initial.py index 8be92f6d3..624b88dd3 100644 --- a/apps/rbac/migrations/0001_initial.py +++ b/apps/rbac/migrations/0001_initial.py @@ -1,16 +1,17 @@ # Generated by Django 3.1.13 on 2021-11-19 08:29 -import common.db.models -from django.conf import settings +import uuid + import django.contrib.auth.models import django.contrib.contenttypes.models -from django.db import migrations, models import django.db.models.deletion -import uuid +from django.conf import settings +from django.db import migrations, models + +import common.db.models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -28,7 +29,8 @@ class Migration(migrations.Migration): ], options={ 'verbose_name': 'Menu permission', - 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workbench view')], + 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), + ('view_workspace', 'Can view workbench view')], 'default_permissions': [], }, ), @@ -41,8 +43,9 @@ class Migration(migrations.Migration): ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')), - ('builtin', models.BooleanField(default=False, verbose_name='Built-in')), + ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', + max_length=128, verbose_name='Scope')), + ('builtin', models.BooleanField(default=False, verbose_name='Builtin')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), ], ), @@ -82,10 +85,15 @@ class Migration(migrations.Migration): ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='orgs.organization', verbose_name='Organization')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='rbac.role', verbose_name='Role')), - ('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', + max_length=128, verbose_name='Scope')), + ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='role_bindings', to='orgs.organization', + verbose_name='Organization')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', + to='rbac.role', verbose_name='Role')), + ('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings', + to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ 'verbose_name': 'Role binding', @@ -95,7 +103,8 @@ class Migration(migrations.Migration): migrations.AddField( model_name='role', name='permissions', - field=models.ManyToManyField(blank=True, related_name='roles', to='rbac.Permission', verbose_name='Permissions'), + field=models.ManyToManyField(blank=True, related_name='roles', to='rbac.Permission', + verbose_name='Permissions'), ), migrations.AlterUniqueTogether( name='role', diff --git a/apps/rbac/migrations/0010_auto_20221220_1956.py b/apps/rbac/migrations/0010_auto_20221220_1956.py new file mode 100644 index 000000000..0dee17c55 --- /dev/null +++ b/apps/rbac/migrations/0010_auto_20221220_1956.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rbac', '0009_auto_20220411_1724'), + ] + + operations = [ + migrations.AddField( + model_name='rolebinding', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='role', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='role', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='rolebinding', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='rolebinding', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + ] diff --git a/apps/rbac/migrations/0011_remove_redundant_permission.py b/apps/rbac/migrations/0011_remove_redundant_permission.py new file mode 100644 index 000000000..74d8412d4 --- /dev/null +++ b/apps/rbac/migrations/0011_remove_redundant_permission.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.16 on 2022-12-27 02:41 + +from django.db import migrations + + +def migrate_remove_redundant_permission(apps, *args): + model = apps.get_model('rbac', 'ContentType') + model.objects.filter(app_label='applications').delete() + model.objects.filter(app_label='ops', model='task').delete() + + model.objects.filter(app_label='xpack', model__in=[ + 'applicationchangeauthplan', 'applicationchangeauthplanexecution', + 'applicationchangeauthplantask', 'changeauthplan', 'changeauthplanexecution', + 'changeauthplantask', 'gatherusertask', 'gatherusertaskexecution' + ]).delete() + + model.objects.filter(app_label='assets', model__in=[ + 'authbook', 'historicalauthbook' + ]).delete() + + model.objects.filter(app_label='perms', model__in=[ + 'applicationpermission', 'permedapplication', 'commandfilterrule', 'historicalauthbook' + ]).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('rbac', '0010_auto_20221220_1956'), + ] + + operations = [ + migrations.RunPython(migrate_remove_redundant_permission) + ] diff --git a/apps/rbac/models/role.py b/apps/rbac/models/role.py index 05783f598..1eff7c15c 100644 --- a/apps/rbac/models/role.py +++ b/apps/rbac/models/role.py @@ -1,11 +1,11 @@ -from django.utils.translation import ugettext_lazy as _, gettext from django.db import models +from django.utils.translation import ugettext_lazy as _, gettext from common.db.models import JMSBaseModel from common.utils import lazyproperty from .permission import Permission -from ..builtin import BuiltinRole from .. import const +from ..builtin import BuiltinRole __all__ = ['Role', 'SystemRole', 'OrgRole'] @@ -33,7 +33,7 @@ class Role(JMSBaseModel): permissions = models.ManyToManyField( 'rbac.Permission', related_name='roles', blank=True, verbose_name=_('Permissions') ) - builtin = models.BooleanField(default=False, verbose_name=_('Built-in')) + builtin = models.BooleanField(default=False, verbose_name=_('Builtin')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) BuiltinRole = BuiltinRole @@ -71,14 +71,14 @@ class Role(JMSBaseModel): @classmethod def get_roles_permissions(cls, roles): org_roles = [role for role in roles if role.scope == cls.Scope.org] - org_perms_id = cls.get_scope_roles_perms(org_roles, cls.Scope.org)\ + org_perms_id = cls.get_scope_roles_perms(org_roles, cls.Scope.org) \ .values_list('id', flat=True) system_roles = [role for role in roles if role.scope == cls.Scope.system] - system_perms_id = cls.get_scope_roles_perms(system_roles, cls.Scope.system)\ + system_perms_id = cls.get_scope_roles_perms(system_roles, cls.Scope.system) \ .values_list('id', flat=True) perms_id = set(org_perms_id) | set(system_perms_id) - permissions = Permission.objects.filter(id__in=perms_id)\ + permissions = Permission.objects.filter(id__in=perms_id) \ .prefetch_related('content_type') return permissions diff --git a/apps/rbac/permissions.py b/apps/rbac/permissions.py index 3b59a4812..1feade002 100644 --- a/apps/rbac/permissions.py +++ b/apps/rbac/permissions.py @@ -93,8 +93,10 @@ class RBACPermission(permissions.DjangoModelPermissions): try: queryset = self._queryset(view) model_cls = queryset.model + except AssertionError: + model_cls = None except Exception as e: - logger.error(e) + logger.error('Error get model class: {} of {}'.format(e, view)) model_cls = None return model_cls diff --git a/apps/rbac/serializers/role.py b/apps/rbac/serializers/role.py index 6070fc8dc..140a01401 100644 --- a/apps/rbac/serializers/role.py +++ b/apps/rbac/serializers/role.py @@ -1,6 +1,7 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from common.drf.fields import LabeledChoiceField from users.models import User from ..models import Role @@ -8,13 +9,13 @@ __all__ = ['RoleSerializer', 'RoleUserSerializer'] class RoleSerializer(serializers.ModelSerializer): - scope_display = serializers.ReadOnlyField(source='get_scope_display', label=_('Scope display')) + scope = LabeledChoiceField(choices=Role.Scope.choices, label=_("Scope")) class Meta: model = Role fields_mini = ['id', 'name', 'display_name', 'scope'] read_only_fields = [ - 'users_amount', 'builtin', 'scope_display', + 'users_amount', 'builtin', 'date_created', 'date_updated', 'created_by', 'updated_by', ] diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index abce5759a..14ed53343 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -33,11 +33,8 @@ view_nodes_data = [ app_nodes_data = [ {'id': 'users', 'view': 'view_console'}, {'id': 'assets', 'view': 'view_console'}, - {'id': 'applications', 'view': 'view_console'}, {'id': 'accounts', 'name': _('Accounts'), 'view': 'view_console'}, {'id': 'perms', 'view': 'view_console'}, - {'id': 'acls', 'view': 'view_console'}, - {'id': 'ops', 'view': 'view_console'}, {'id': 'terminal', 'name': _('Session audits'), 'view': 'view_audit'}, {'id': 'audits', 'view': 'view_audit'}, {'id': 'rbac', 'view': 'view_console'}, @@ -51,33 +48,42 @@ extra_nodes_data = [ {"id": "cloud_import", "name": _("Cloud import"), "pId": "assets"}, {"id": "backup_account_node", "name": _("Backup account"), "pId": "accounts"}, {"id": "gather_account_node", "name": _("Gather account"), "pId": "accounts"}, - {"id": "app_change_plan_node", "name": _("App change auth"), "pId": "accounts"}, {"id": "asset_change_plan_node", "name": _("Asset change auth"), "pId": "accounts"}, {"id": "terminal_node", "name": _("Terminal setting"), "pId": "view_setting"}, + {'id': "task_center", "name": _("Task Center"), "pId": "view_console"}, {'id': "my_assets", "name": _("My assets"), "pId": "view_workbench"}, - {'id': "my_apps", "name": _("My apps"), "pId": "view_workbench"}, + {'id': "operation_center", "name": _('App ops'), "pId": "view_workbench"}, + {'id': "remote_application", "name": _("Applet"), "pId": "view_setting"}, ] # 将 model 放到其它节点下,而不是本来的 app 中 special_pid_mapper = { 'common.permission': 'view_other', - "assets.account": "accounts", - "applications.account": "accounts", + 'assets.account': 'accounts', + 'assets.accounttemplate': 'accounts', + 'acls.commandfilteracl': 'perms', + 'acls.commandgroup': 'perms', + 'acls.loginacl': 'perms', + 'acls.loginassetacl': 'perms', 'xpack.account': 'cloud_import', 'xpack.syncinstancedetail': 'cloud_import', 'xpack.syncinstancetask': 'cloud_import', 'xpack.syncinstancetaskexecution': 'cloud_import', + 'terminal.applet': 'remote_application', + 'terminal.applethost': 'remote_application', 'assets.accountbackupplan': "backup_account_node", 'assets.accountbackupplanexecution': "backup_account_node", - 'xpack.applicationchangeauthplan': 'app_change_plan_node', - 'xpack.applicationchangeauthplanexecution': 'app_change_plan_node', - 'xpack.applicationchangeauthplantask': 'app_change_plan_node', 'xpack.changeauthplan': 'asset_change_plan_node', 'xpack.changeauthplanexecution': 'asset_change_plan_node', 'xpack.changeauthplantask': 'asset_change_plan_node', "assets.gathereduser": "gather_account_node", - 'xpack.gatherusertask': 'gather_account_node', - 'xpack.gatherusertaskexecution': 'gather_account_node', + "assets.gatheraccountsautomation": "gather_account_node", + "assets.view_gatheraccountsexecution": "gather_account_node", + "assets.add_gatheraccountsexecution": "gather_account_node", + "assets.changesecretautomation": "asset_change_plan_node", + "assets.view_changesecretexecution": "asset_change_plan_node", + "assets.add_changesecretexection": "asset_change_plan_node", + "assets.view_changesecretrecord": "asset_change_plan_node", 'orgs.organization': 'view_setting', 'settings.setting': 'view_setting', 'terminal.terminal': 'terminal_node', @@ -89,15 +95,17 @@ special_pid_mapper = { 'terminal.endpointrule': 'terminal_node', 'audits.ftplog': 'terminal', 'perms.view_myassets': 'my_assets', - 'perms.view_myapps': 'my_apps', 'ops.add_commandexecution': 'view_workbench', 'ops.view_commandexecution': 'audits', - "perms.view_mykubernetsapp": "my_apps", - "perms.connect_mykubernetsapp": "my_apps", - "perms.view_myremoteapp": "my_apps", - "perms.connect_myremoteapp": "my_apps", - "perms.view_mydatabaseapp": "my_apps", - "perms.connect_mydatabaseapp": "my_apps", + 'ops.jobauditlog': 'audits', + 'ops.view_celerytask': 'task_center', + 'ops.view_celerytaskexecution': 'task_center', + 'ops.view_taskmonitor': 'task_center', + 'ops.adhocexecution': 'task_center', + 'ops.job': 'operation_center', + 'ops.adhoc': 'operation_center', + 'ops.playbook': 'operation_center', + 'ops.jobexecution': 'operation_center', "xpack.interface": "view_setting", "settings.change_terminal": "terminal_node", "settings.view_setting": "view_setting", diff --git a/apps/settings/models.py b/apps/settings/models.py index 8506a0fa1..170d032a4 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -43,6 +43,9 @@ class Setting(models.Model): def __str__(self): return self.name + def is_name(self, name): + return self.name == name + @property def cleaned_value(self): try: diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index ea25ca9d7..259cf9712 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -38,6 +38,11 @@ class CommonSettingSerializer(serializers.Serializer): help_text=_('User attr map present how to map OpenID user attr to ' 'jumpserver, username,name,email is jumpserver attr') ) + AUTH_OPENID_PKCE = serializers.BooleanField(required=False, label=_('Enable PKCE')) + AUTH_OPENID_CODE_CHALLENGE_METHOD = serializers.ChoiceField( + default='S256', label=_('Code challenge method'), + choices=(('S256', 'HS256'), ('plain', 'Plain')) + ) class KeycloakSettingSerializer(CommonSettingSerializer): diff --git a/apps/settings/signal_handlers.py b/apps/settings/signal_handlers.py index c963488f1..7568de368 100644 --- a/apps/settings/signal_handlers.py +++ b/apps/settings/signal_handlers.py @@ -8,23 +8,21 @@ from django.db.utils import ProgrammingError, OperationalError from django.dispatch import receiver from django.utils.functional import LazyObject +from jumpserver.utils import current_request + from common.decorator import on_transaction_commit from common.signals import django_ready from common.utils import get_logger, ssh_key_gen from common.utils.connection import RedisPubSub -from jumpserver.utils import current_request + from .models import Setting logger = get_logger(__file__) -def get_settings_pub_sub(): - return RedisPubSub('settings') - - class SettingSubPub(LazyObject): def _setup(self): - self._wrapped = get_settings_pub_sub() + self._wrapped = RedisPubSub('settings') setting_pub_sub = SettingSubPub() @@ -35,15 +33,12 @@ setting_pub_sub = SettingSubPub() def refresh_settings_on_changed(sender, instance=None, **kwargs): if not instance: return - setting_pub_sub.publish(instance.name) - - # 配置变化: PERM_SINGLE_ASSET_TO_UNGROUP_NODE - if instance.name == 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': - # 清除所有用户授权树已构建的标记,下次访问重新生成 - logger.debug('Clean ALL User perm tree built mark') - from perms.utils.asset import UserGrantedTreeRefreshController - UserGrantedTreeRefreshController.clean_all_user_tree_built_mark() + if instance.is_name('PERM_SINGLE_ASSET_TO_UNGROUP_NODE'): + """ 过期所有用户授权树 """ + logger.debug('Expire all user perm tree') + from perms.utils import UserPermTreeExpireUtil + UserPermTreeExpireUtil().expire_perm_tree_for_all_user() @receiver(django_ready) diff --git a/apps/static/img/login_image.jpg b/apps/static/img/login_image.jpg deleted file mode 100644 index 0ca1c9107..000000000 Binary files a/apps/static/img/login_image.jpg and /dev/null differ diff --git a/apps/static/img/login_image.png b/apps/static/img/login_image.png new file mode 100644 index 000000000..936bcd81b Binary files /dev/null and b/apps/static/img/login_image.png differ diff --git a/apps/static/img/login_image02.jpg b/apps/static/img/login_image02.jpg deleted file mode 100644 index 9c9958506..000000000 Binary files a/apps/static/img/login_image02.jpg and /dev/null differ diff --git a/apps/static/img/logo_text_white.png b/apps/static/img/logo_text_white.png index f791baa71..b793b9e5b 100644 Binary files a/apps/static/img/logo_text_white.png and b/apps/static/img/logo_text_white.png differ diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index a00814dd3..fddf90ad1 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -1,23 +1,22 @@ +import os.path import shutil import zipfile -import yaml -import os.path from typing import Callable -from django.http import HttpResponse +from django.conf import settings from django.core.files.storage import default_storage +from django.http import HttpResponse from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ValidationError -from common.utils import is_uuid from common.drf.serializers import FileSerializer +from common.utils import is_uuid from terminal import serializers from terminal.models import AppletPublication, Applet - __all__ = ['AppletViewSet', 'AppletPublicationViewSet'] @@ -46,17 +45,7 @@ class DownloadUploadMixin: zp.extractall(extract_to) tmp_dir = os.path.join(extract_to, file.name.replace('.zip', '')) - files = ['manifest.yml', 'icon.png', 'i18n.yml', 'setup.yml'] - for name in files: - path = os.path.join(tmp_dir, name) - if not os.path.exists(path): - raise ValidationError({'error': 'Missing file {}'.format(name)}) - - with open(os.path.join(tmp_dir, 'manifest.yml')) as f: - manifest = yaml.safe_load(f) - - if not manifest.get('name', ''): - raise ValidationError({'error': 'Missing name in manifest.yml'}) + manifest = Applet.validate_pkg(tmp_dir) return manifest, tmp_dir @action(detail=False, methods=['post'], serializer_class=FileSerializer) @@ -81,11 +70,15 @@ class DownloadUploadMixin: @action(detail=True, methods=['get']) def download(self, request, *args, **kwargs): instance = self.get_object() - path = default_storage.path('applets/{}'.format(instance.name)) + if instance.builtin: + path = os.path.join(settings.APPS_DIR, 'terminal', 'applets', instance.name) + else: + path = default_storage.path('applets/{}'.format(instance.name)) zip_path = shutil.make_archive(path, 'zip', path) with open(zip_path, 'rb') as f: response = HttpResponse(f.read(), status=200, content_type='application/octet-stream') response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'{}.zip'.format(instance.name) + os.unlink(zip_path) return response diff --git a/apps/terminal/api/component/__init__.py b/apps/terminal/api/component/__init__.py index afefe0c18..56432ca42 100644 --- a/apps/terminal/api/component/__init__.py +++ b/apps/terminal/api/component/__init__.py @@ -1,4 +1,5 @@ -from .terminal import * -from .storage import * -from .status import * +from .connect_methods import * from .endpoint import * +from .status import * +from .storage import * +from .terminal import * diff --git a/apps/terminal/api/component/connect_methods.py b/apps/terminal/api/component/connect_methods.py new file mode 100644 index 000000000..a284159d3 --- /dev/null +++ b/apps/terminal/api/component/connect_methods.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import generics +from rest_framework.views import Response + +from common.permissions import IsValidUser +from common.utils import get_request_os +from terminal import serializers +from terminal.connect_methods import ConnectMethodUtil + +__all__ = ['ConnectMethodListApi'] + + +class ConnectMethodListApi(generics.ListAPIView): + serializer_class = serializers.ConnectMethodSerializer + permission_classes = [IsValidUser] + + def get_queryset(self): + os = get_request_os(self.request) + return ConnectMethodUtil.get_protocols_connect_methods(os) + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + return Response(queryset) diff --git a/apps/terminal/api/component/terminal.py b/apps/terminal/api/component/terminal.py index df14296f5..d32adf02b 100644 --- a/apps/terminal/api/component/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -10,16 +10,13 @@ from rest_framework.views import APIView, Response from common.drf.api import JMSBulkModelViewSet from common.exceptions import JMSException -from common.permissions import IsValidUser from common.permissions import WithBootstrapToken -from common.utils import get_request_os from terminal import serializers -from terminal.const import TerminalType from terminal.models import Terminal __all__ = [ 'TerminalViewSet', 'TerminalConfig', - 'TerminalRegistrationApi', 'ConnectMethodListApi' + 'TerminalRegistrationApi', ] logger = logging.getLogger(__file__) @@ -72,15 +69,3 @@ class TerminalRegistrationApi(generics.CreateAPIView): return Response(data=data, status=status.HTTP_400_BAD_REQUEST) return super().create(request, *args, **kwargs) - -class ConnectMethodListApi(generics.ListAPIView): - serializer_class = serializers.ConnectMethodSerializer - permission_classes = [IsValidUser] - - def get_queryset(self): - os = get_request_os(self.request) - return TerminalType.get_protocols_connect_methods(os) - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - return Response(queryset) diff --git a/apps/terminal/api/db_listen_port.py b/apps/terminal/api/db_listen_port.py index 4170a33d4..69dee6229 100644 --- a/apps/terminal/api/db_listen_port.py +++ b/apps/terminal/api/db_listen_port.py @@ -7,6 +7,7 @@ from rest_framework.viewsets import GenericViewSet from ..utils import db_port_manager, DBPortManager from applications import serializers +from assets.serializers.asset.database import DatabaseSerializer db_port_manager: DBPortManager @@ -32,5 +33,5 @@ class DBListenPortViewSet(GenericViewSet): def db_info(self, request, *args, **kwargs): port = request.query_params.get("port") db = db_port_manager.get_db_by_port(port) - serializer = serializers.AppSerializer(instance=db) + serializer = DatabaseSerializer(instance=db) return Response(data=serializer.data, status=status.HTTP_200_OK) diff --git a/apps/terminal/applets/__init__.py b/apps/terminal/applets/__init__.py new file mode 100644 index 000000000..bb09feecd --- /dev/null +++ b/apps/terminal/applets/__init__.py @@ -0,0 +1,22 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def install_or_update_builtin_applets(): + from terminal.models import Applet + + applets = os.listdir(BASE_DIR) + for d in applets: + path = os.path.join(BASE_DIR, d) + if not os.path.isdir(path) or not os.path.exists(os.path.join(path, 'manifest.yml')): + continue + print("Install or update applet: {}".format(path)) + try: + Applet.install_from_dir(path) + except Exception as e: + print(e) + + +if __name__ == '__main__': + install_or_update_builtin_applets() diff --git a/apps/terminal/applets/chrome/README.md b/apps/terminal/applets/chrome/README.md new file mode 100644 index 000000000..068682bfb --- /dev/null +++ b/apps/terminal/applets/chrome/README.md @@ -0,0 +1,7 @@ + +## selenium 版本 + +- Selenium == 4.4.0 +- Chrome 和 ChromeDriver 版本要匹配 +- Driver [下载地址](https://chromedriver.chromium.org/downloads) + diff --git a/apps/terminal/applets/chrome/app.py b/apps/terminal/applets/chrome/app.py new file mode 100644 index 000000000..78e3e2c5a --- /dev/null +++ b/apps/terminal/applets/chrome/app.py @@ -0,0 +1,201 @@ +import time +from enum import Enum +from subprocess import CREATE_NO_WINDOW + +from selenium import webdriver +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.service import Service + +from common import (Asset, User, Account, Platform) +from common import (notify_err_message, block_input, unblock_input) +from common import (BaseApplication) + + +class Command(Enum): + TYPE = 'type' + CLICK = 'click' + OPEN = 'open' + + +def _execute_type(ele: WebElement, value: str): + ele.send_keys(value) + + +def _execute_click(ele: WebElement, value: str): + ele.click() + + +commands_func_maps = { + Command.CLICK: _execute_click, + Command.TYPE: _execute_type, + Command.OPEN: _execute_type, +} + + +class StepAction: + methods_map = { + "NAME": By.NAME, + "ID": By.ID, + "CLASS_NAME": By.CLASS_NAME, + "CSS_SELECTOR": By.CSS_SELECTOR, + "CSS": By.CSS_SELECTOR, + "XPATH": By.XPATH + } + + def __init__(self, target='', value='', command=Command.TYPE, **kwargs): + self.target = target + self.value = value + self.command = command + + def execute(self, driver: webdriver.Chrome) -> bool: + if not self.target: + return True + target_name, target_value = self.target.split("=", 1) + by_name = self.methods_map.get(target_name.upper(), By.NAME) + ele = driver.find_element(by=by_name, value=target_value) + if not ele: + return False + if self.command == 'type': + ele.send_keys(self.value) + elif self.command in ['click', 'button']: + ele.click() + elif self.command in ['open']: + driver.get(self.value) + return True + + def _execute_command_type(self, ele, value): + ele.send_keys(value) + + +def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool: + try: + return step.execute(driver) + except Exception as e: + print(e) + notify_err_message(str(e)) + return False + + +class WebAPP(object): + + def __init__(self, app_name: str = '', user: User = None, asset: Asset = None, + account: Account = None, platform: Platform = None, **kwargs): + self.app_name = app_name + self.user = user + self.asset = asset + self.account = account + self.platform = platform + + self.extra_data = self.asset.specific + self._steps = list() + autofill_type = self.asset.specific.autofill + if autofill_type == "basic": + self._steps = self._default_custom_steps() + elif autofill_type == "script": + steps = sorted(self.asset.specific.script, key=lambda step_item: step_item['step']) + for item in steps: + val = item['value'] + if val: + val = val.replace("{USERNAME}", self.account.username) + val = val.replace("{SECRET}", self.account.secret) + item['value'] = val + self._steps.append(item) + + def _default_custom_steps(self) -> list: + account = self.account + specific_property = self.asset.specific + default_steps = [ + { + "step": 1, + "value": account.username, + "target": specific_property.username_selector, + "command": "type" + }, + { + "step": 2, + "value": account.secret, + "target": specific_property.password_selector, + "command": "type" + }, + { + "step": 3, + "value": "", + "target": specific_property.submit_selector, + "command": "click" + } + ] + return default_steps + + def execute(self, driver: webdriver.Chrome) -> bool: + if not self.asset.address: + return True + + for step in self._steps: + action = StepAction(**step) + ret = execute_action(driver, action) + if not ret: + unblock_input() + notify_err_message(f"执行失败: target: {action.target} command: {action.command}") + block_input() + return False + return True + + +def default_chrome_driver_options(): + options = webdriver.ChromeOptions() + options.add_argument("start-maximized") + # 禁用 扩展 + options.add_argument("--disable-extensions") + # 禁用开发者工具 + options.add_argument("--disable-dev-tools") + # 禁用 密码管理器弹窗 + prefs = {"credentials_enable_service": False, + "profile.password_manager_enabled": False} + options.add_experimental_option("prefs", prefs) + options.add_experimental_option("excludeSwitches", ['enable-automation']) + return options + + +class AppletApplication(BaseApplication): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.driver = None + self.app = WebAPP(app_name=self.app_name, user=self.user, + account=self.account, asset=self.asset, platform=self.platform) + self._chrome_options = default_chrome_driver_options() + + def run(self): + service = Service() + # driver 的 console 终端框不显示 + service.creationflags = CREATE_NO_WINDOW + self.driver = webdriver.Chrome(options=self._chrome_options, service=service) + self.driver.implicitly_wait(10) + if self.app.asset.address != "": + self.driver.get(self.app.asset.address) + ok = self.app.execute(self.driver) + if not ok: + print("执行失败") + self.driver.maximize_window() + + def wait(self): + msg = "Unable to evaluate script: disconnected: not connected to DevTools\n" + while True: + time.sleep(5) + logs = self.driver.get_log('driver') + if len(logs) == 0: + continue + ret = logs[-1] + if isinstance(ret, dict): + if ret.get("message") == msg: + print(ret) + break + self.close() + + def close(self): + if self.driver: + try: + self.driver.close() + except Exception as e: + print(e) diff --git a/apps/terminal/applets/chrome/common.py b/apps/terminal/applets/chrome/common.py new file mode 100644 index 000000000..75e59a4c1 --- /dev/null +++ b/apps/terminal/applets/chrome/common.py @@ -0,0 +1,197 @@ +import abc +import subprocess +import sys +import time +import os +import json +import base64 +from subprocess import CREATE_NO_WINDOW + +_blockInput = None +_messageBox = None +if sys.platform == 'win32': + import ctypes + from ctypes import wintypes + import win32ui + + # import win32con + + _messageBox = win32ui.MessageBox + + _blockInput = ctypes.windll.user32.BlockInput + _blockInput.argtypes = [wintypes.BOOL] + _blockInput.restype = wintypes.BOOL + + +def block_input(): + if _blockInput: + _blockInput(True) + + +def unblock_input(): + if _blockInput: + _blockInput(False) + + +def notify_err_message(msg): + if _messageBox: + _messageBox(msg, 'Error') + + +def check_pid_alive(pid) -> bool: + # tasklist /fi "PID eq 508" /fo csv + # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' + try: + + csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], + creationflags=CREATE_NO_WINDOW) + content = csv_ret.decode() + content_list = content.strip().split("\r\n") + if len(content_list) != 2: + notify_err_message(content) + return False + ret_pid = content_list[1].split(",")[1].strip('"') + return str(pid) == ret_pid + except Exception as e: + notify_err_message(e) + return False + + +def wait_pid(pid): + while 1: + time.sleep(5) + ok = check_pid_alive(pid) + if not ok: + notify_err_message("程序退出") + break + + +class DictObj: + def __init__(self, in_dict: dict): + assert isinstance(in_dict, dict) + for key, val in in_dict.items(): + if isinstance(val, (list, tuple)): + setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val]) + else: + setattr(self, key, DictObj(val) if isinstance(val, dict) else val) + + +class User(DictObj): + id: str + name: str + username: str + + +class Specific(DictObj): + # web + autofill: str + username_selector: str + password_selector: str + submit_selector: str + script: list + + # database + db_name: str + + +class Category(DictObj): + value: str + label: str + + +class Protocol(DictObj): + id: str + name: str + port: int + + +class Asset(DictObj): + id: str + name: str + address: str + protocols: list[Protocol] + category: Category + specific: Specific + + def get_protocol_port(self, protocol): + for item in self.protocols: + if item.name == protocol: + return item.port + return None + + +class LabelValue(DictObj): + label: str + value: str + + +class Account(DictObj): + id: str + name: str + username: str + secret: str + secret_type: LabelValue + + +class Platform(DictObj): + id: str + name: str + charset: LabelValue + type: LabelValue + + +class Manifest(DictObj): + name: str + version: str + path: str + exec_type: str + connect_type: str + protocols: list[str] + + +def get_manifest_data() -> dict: + current_dir = os.path.dirname(__file__) + manifest_file = os.path.join(current_dir, 'manifest.json') + try: + with open(manifest_file, "r", encoding='utf8') as f: + return json.load(f) + except Exception as e: + print(e) + return {} + + +def read_app_manifest(app_dir) -> dict: + main_json_file = os.path.join(app_dir, "manifest.json") + if not os.path.exists(main_json_file): + return {} + with open(main_json_file, 'r', encoding='utf8') as f: + return json.load(f) + + +def convert_base64_to_dict(base64_str: str) -> dict: + try: + data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8') + return json.loads(data_json) + except Exception as e: + print(e) + return {} + + +class BaseApplication(abc.ABC): + + def __init__(self, *args, **kwargs): + self.app_name = kwargs.get('app_name', '') + self.protocol = kwargs.get('protocol', '') + self.manifest = Manifest(kwargs.get('manifest', {})) + self.user = User(kwargs.get('user', {})) + self.asset = Asset(kwargs.get('asset', {})) + self.account = Account(kwargs.get('account', {})) + self.platform = Platform(kwargs.get('platform', {})) + + @abc.abstractmethod + def run(self): + raise NotImplementedError('run') + + @abc.abstractmethod + def wait(self): + raise NotImplementedError('wait') diff --git a/apps/terminal/applets/chrome/i18n.yml b/apps/terminal/applets/chrome/i18n.yml new file mode 100644 index 000000000..d91977ba6 --- /dev/null +++ b/apps/terminal/applets/chrome/i18n.yml @@ -0,0 +1,4 @@ +- zh: + display_name: Chrome 浏览器 + comment: 浏览器打开 URL 页面地址 + diff --git a/apps/terminal/applets/chrome/icon.png b/apps/terminal/applets/chrome/icon.png new file mode 100644 index 000000000..ee35972d8 Binary files /dev/null and b/apps/terminal/applets/chrome/icon.png differ diff --git a/apps/terminal/applets/chrome/main.py b/apps/terminal/applets/chrome/main.py new file mode 100644 index 000000000..be0ff3585 --- /dev/null +++ b/apps/terminal/applets/chrome/main.py @@ -0,0 +1,22 @@ +import sys + +from common import (block_input, unblock_input) +from common import convert_base64_to_dict +from app import AppletApplication + + +def main(): + base64_str = sys.argv[1] + data = convert_base64_to_dict(base64_str) + applet_app = AppletApplication(**data) + block_input() + applet_app.run() + unblock_input() + applet_app.wait() + + +if __name__ == '__main__': + try: + main() + except Exception as e: + print(e) diff --git a/apps/terminal/applets/chrome/manifest.yml b/apps/terminal/applets/chrome/manifest.yml new file mode 100644 index 000000000..f2681a0c2 --- /dev/null +++ b/apps/terminal/applets/chrome/manifest.yml @@ -0,0 +1,12 @@ +name: chrome +display_name: Chrome Browser +version: 0.1 +comment: Chrome Browser Open URL Page Address +author: JumpServer Team +exec_type: python +update_policy: always +type: web +tags: + - web +protocols: + - http diff --git a/apps/terminal/applets/chrome/setup.yml b/apps/terminal/applets/chrome/setup.yml new file mode 100644 index 000000000..9b9a950f1 --- /dev/null +++ b/apps/terminal/applets/chrome/setup.yml @@ -0,0 +1,6 @@ +type: manual # exe, zip, manual +source: +arguments: +destination: +program: +md5: diff --git a/apps/terminal/applets/chrome/test_data_example.json b/apps/terminal/applets/chrome/test_data_example.json new file mode 100644 index 000000000..fc8e00991 --- /dev/null +++ b/apps/terminal/applets/chrome/test_data_example.json @@ -0,0 +1,41 @@ +{ + "protocol": "web", + "user": { + "id": "2647CA35-5CAD-4DDF-8A88-6BD88F39BB30", + "name": "Administrator", + "username": "admin" + }, + "asset": { + "id": "46EE5F50-F1C1-468C-97EE-560E3436754C", + "name": "test_baidu", + "address": "https://www.baidu.com", + "category": { + "value": "web", + "label": "web" + }, + "protocols": [ + { + "id": 2, + "name": "http", + "port": 80 + } + ], + "specific": { + "autofill": "basic", + "username_selector": "name=username", + "password_selector": "name=password", + "submit_selector": "id=longin_button", + "script": [] + }, + "org_id": "2925D985-A435-411D-9BC4-FEA630F105D9" + }, + "account": { + "id": "9D5585DE-5132-458C-AABE-89A83C112A83", + "name": "test_mysql", + "username": "root", + "secret": "" + }, + "platform": { + "charset": "UTF-8" + } +} diff --git a/apps/terminal/backends/command/serializers.py b/apps/terminal/backends/command/serializers.py index ccf6984b8..e799f92f2 100644 --- a/apps/terminal/backends/command/serializers.py +++ b/apps/terminal/backends/command/serializers.py @@ -36,16 +36,10 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer): # 限制 64 字符,不能直接迁移成 128 字符,命令表数据量会比较大 account = serializers.CharField(label=_("Account ")) output = serializers.CharField(max_length=2048, allow_blank=True, label=_("Output")) - risk_level_display = serializers.SerializerMethodField(label=_('Risk level display')) timestamp = serializers.IntegerField(label=_('Timestamp')) timestamp_display = serializers.DateTimeField(read_only=True, label=_('Datetime')) remote_addr = serializers.CharField(read_only=True, label=_('Remote Address')) - @staticmethod - def get_risk_level_display(obj): - risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES) - return risk_mapper.get(obj.risk_level) - def validate_account(self, value): if len(value) > 64: value = pretty_string(value, 64) diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py new file mode 100644 index 000000000..454abb522 --- /dev/null +++ b/apps/terminal/connect_methods.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# +from collections import defaultdict + +from django.conf import settings +from django.db.models import TextChoices +from django.utils.translation import ugettext_lazy as _ + +from assets.const import Protocol +from .const import TerminalType + + +class WebMethod(TextChoices): + web_gui = 'web_gui', 'Web GUI' + web_cli = 'web_cli', 'Web CLI' + web_sftp = 'web_sftp', 'Web SFTP' + + @classmethod + def get_methods(cls): + return { + Protocol.ssh: [cls.web_cli, cls.web_sftp], + Protocol.telnet: [cls.web_cli], + Protocol.rdp: [cls.web_gui], + Protocol.vnc: [cls.web_gui], + + Protocol.mysql: [cls.web_cli, cls.web_gui], + Protocol.mariadb: [cls.web_cli, cls.web_gui], + Protocol.oracle: [cls.web_cli, cls.web_gui], + Protocol.postgresql: [cls.web_cli, cls.web_gui], + Protocol.sqlserver: [cls.web_cli, cls.web_gui], + Protocol.redis: [cls.web_cli], + Protocol.mongodb: [cls.web_cli], + + Protocol.k8s: [cls.web_cli], + Protocol.http: [] + } + + +class NativeClient(TextChoices): + # Koko + ssh = 'ssh', 'SSH' + putty = 'putty', 'PuTTY' + xshell = 'xshell', 'Xshell' + + # Magnus + mysql = 'db_client_mysql', _('DB Client') + psql = 'db_client_psql', _('DB Client') + sqlplus = 'db_client_sqlplus', _('DB Client') + redis = 'db_client_redis', _('DB Client') + mongodb = 'db_client_mongodb', _('DB Client') + + # Razor + mstsc = 'mstsc', 'Remote Desktop' + + @classmethod + def get_native_clients(cls): + # native client 关注的是 endpoint 的 protocol, + # 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议 + clients = { + Protocol.ssh: { + 'default': [cls.ssh], + 'windows': [cls.putty], + }, + Protocol.rdp: [cls.mstsc], + Protocol.mysql: [cls.mysql], + Protocol.oracle: [cls.sqlplus], + Protocol.postgresql: [cls.psql], + Protocol.redis: [cls.redis], + Protocol.mongodb: [cls.mongodb], + } + return clients + + @classmethod + def get_target_protocol(cls, name, os): + for protocol, clients in cls.get_native_clients().items(): + if isinstance(clients, dict): + clients = clients.get(os) or clients.get('default') + if name in clients: + return protocol + return None + + @classmethod + def xpack_methods(cls): + return [cls.sqlplus, cls.mstsc] + + @classmethod + def get_methods(cls, os='windows'): + clients_map = cls.get_native_clients() + methods = defaultdict(list) + + for protocol, _clients in clients_map.items(): + if isinstance(_clients, dict): + _clients = _clients.get(os, _clients['default']) + for client in _clients: + if not settings.XPACK_ENABLED and client in cls.xpack_methods(): + continue + methods[protocol].append({ + 'value': client.value, + 'label': client.label, + 'type': 'native', + }) + return methods + + @classmethod + def get_launch_command(cls, name, token, endpoint, os='windows'): + username = f'JMS-{token.id}' + commands = { + cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}', + cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}', + cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', + # cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p', + # cls.psql: { + # 'default': 'psql -h {hostname} -p {port} -U {username} -W', + # 'windows': 'psql /h {hostname} /p {port} /U {username} -W', + # }, + # cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}', + # cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}', + } + command = commands.get(name) + if isinstance(command, dict): + command = command.get(os, command.get('default')) + return command + + +class AppletMethod: + @classmethod + def get_methods(cls): + from .models import Applet, AppletHost + + methods = defaultdict(list) + if not settings.XPACK_ENABLED: + return methods + + has_applet_hosts = AppletHost.objects.all().exists() + applets = Applet.objects.filter(is_active=True) + for applet in applets: + for protocol in applet.protocols: + methods[protocol].append({ + 'value': applet.name, + 'label': applet.display_name, + 'type': 'applet', + 'icon': applet.icon, + 'disabled': not applet.is_active or not has_applet_hosts, + }) + return methods + + +class ConnectMethodUtil: + _all_methods = None + + @classmethod + def protocols(cls): + protocols = { + TerminalType.koko: { + 'web_methods': [WebMethod.web_cli, WebMethod.web_sftp], + 'listen': [Protocol.http], + 'support': [ + Protocol.ssh, Protocol.telnet, + Protocol.mysql, Protocol.postgresql, + Protocol.oracle, Protocol.sqlserver, + Protocol.mariadb, Protocol.redis, + Protocol.mongodb, Protocol.k8s, + Protocol.clickhouse, + ], + 'match': 'm2m' + }, + TerminalType.omnidb: { + 'web_methods': [WebMethod.web_gui], + 'listen': [Protocol.http], + 'support': [ + Protocol.mysql, Protocol.postgresql, Protocol.oracle, + Protocol.sqlserver, Protocol.mariadb + ], + 'match': 'm2m' + }, + TerminalType.lion: { + 'web_methods': [WebMethod.web_gui], + 'listen': [Protocol.http], + 'support': [Protocol.rdp, Protocol.vnc], + 'match': 'm2m' + }, + TerminalType.magnus: { + 'listen': [], + 'support': [ + Protocol.mysql, Protocol.postgresql, + Protocol.oracle, Protocol.mariadb + ], + 'match': 'map' + }, + TerminalType.razor: { + 'listen': [Protocol.rdp], + 'support': [Protocol.rdp], + 'match': 'map' + }, + } + return protocols + + @classmethod + def get_connect_method(cls, name, protocol, os='linux'): + methods = cls.get_protocols_connect_methods(os) + protocol_methods = methods.get(protocol, []) + for method in protocol_methods: + if method['value'] == name: + return method + return None + + @classmethod + def refresh_methods(cls): + cls._all_methods = None + + @classmethod + def get_protocols_connect_methods(cls, os): + if cls._all_methods is not None: + return cls._all_methods + + methods = defaultdict(list) + web_methods = WebMethod.get_methods() + native_methods = NativeClient.get_methods(os) + applet_methods = AppletMethod.get_methods() + + for component, component_protocol in cls.protocols().items(): + support = component_protocol['support'] + component_web_methods = component_protocol.get('web_methods', []) + + for protocol in support: + # Web 方式 + methods[protocol.value].extend([ + { + 'component': component.value, + 'type': 'web', + 'endpoint_protocol': 'http', + 'value': method.value, + 'label': method.label, + } + for method in web_methods.get(protocol, []) + if method in component_web_methods + ]) + + # 客户端方式 + if component_protocol['match'] == 'map': + listen = [protocol] + else: + listen = component_protocol['listen'] + + for listen_protocol in listen: + # Native method + methods[protocol.value].extend([ + { + 'component': component.value, + 'type': 'native', + 'endpoint_protocol': listen_protocol, + **method + } + for method in native_methods[listen_protocol] + ]) + + # 远程应用方式,这个只有 tinker 提供 + for protocol, applet_methods in applet_methods.items(): + for method in applet_methods: + method['listen'] = 'rdp' + method['component'] = TerminalType.tinker.value + methods[protocol].extend(applet_methods) + + cls._all_methods = methods + return methods diff --git a/apps/terminal/const.py b/apps/terminal/const.py index bf3416c91..69f994476 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -1,16 +1,9 @@ # -*- coding: utf-8 -*- # -from collections import defaultdict from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -from assets.const import Protocol - - -# Replay & Command Storage Choices -# -------------------------------- - class ReplayStorageType(TextChoices): null = 'null', 'Null', @@ -44,129 +37,6 @@ class ComponentLoad(TextChoices): return set(dict(cls.choices).keys()) -class WebMethod(TextChoices): - web_gui = 'web_gui', 'Web GUI' - web_cli = 'web_cli', 'Web CLI' - web_sftp = 'web_sftp', 'Web SFTP' - - @classmethod - def get_methods(cls): - return { - Protocol.ssh: [cls.web_cli, cls.web_sftp], - Protocol.telnet: [cls.web_cli], - Protocol.rdp: [cls.web_gui], - Protocol.vnc: [cls.web_gui], - - Protocol.mysql: [cls.web_cli, cls.web_gui], - Protocol.mariadb: [cls.web_cli, cls.web_gui], - Protocol.oracle: [cls.web_cli, cls.web_gui], - Protocol.postgresql: [cls.web_cli, cls.web_gui], - Protocol.sqlserver: [cls.web_cli, cls.web_gui], - Protocol.clickhouse: [cls.web_cli], - Protocol.redis: [cls.web_cli], - Protocol.mongodb: [cls.web_cli], - - Protocol.k8s: [cls.web_gui], - Protocol.http: [] - } - - -class NativeClient(TextChoices): - # Koko - ssh = 'ssh', 'SSH' - putty = 'putty', 'PuTTY' - xshell = 'xshell', 'Xshell' - - # Magnus - mysql = 'db_client_mysql', _('DB Client') - psql = 'db_client_psql', _('DB Client') - sqlplus = 'db_client_sqlplus', _('DB Client') - redis = 'db_client_redis', _('DB Client') - mongodb = 'db_client_mongodb', _('DB Client') - - # Razor - mstsc = 'mstsc', 'Remote Desktop' - - @classmethod - def get_native_clients(cls): - # native client 关注的是 endpoint 的 protocol, - # 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议 - clients = { - Protocol.ssh: { - 'default': [cls.ssh], - 'windows': [cls.putty], - }, - Protocol.rdp: [cls.mstsc], - Protocol.mysql: [cls.mysql], - Protocol.oracle: [cls.sqlplus], - Protocol.postgresql: [cls.psql], - Protocol.redis: [cls.redis], - Protocol.mongodb: [cls.mongodb], - } - return clients - - @classmethod - def get_target_protocol(cls, name, os): - for protocol, clients in cls.get_native_clients().items(): - if isinstance(clients, dict): - clients = clients.get(os) or clients.get('default') - if name in clients: - return protocol - return None - - @classmethod - def get_methods(cls, os='windows'): - clients_map = cls.get_native_clients() - methods = defaultdict(list) - - for protocol, _clients in clients_map.items(): - if isinstance(_clients, dict): - _clients = _clients.get(os, _clients['default']) - for client in _clients: - methods[protocol].append({ - 'value': client.value, - 'label': client.label, - 'type': 'native', - }) - return methods - - @classmethod - def get_launch_command(cls, name, token, endpoint, os='windows'): - username = f'JMS-{token.id}' - commands = { - cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}', - cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}', - cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', - # cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p', - # cls.psql: { - # 'default': 'psql -h {hostname} -p {port} -U {username} -W', - # 'windows': 'psql /h {hostname} /p {port} /U {username} -W', - # }, - # cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}', - # cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}', - } - command = commands.get(name) - if isinstance(command, dict): - command = command.get(os, command.get('default')) - return command - - -class AppletMethod: - @classmethod - def get_methods(cls): - from .models import Applet - applets = Applet.objects.all() - methods = defaultdict(list) - for applet in applets: - for protocol in applet.protocols: - methods[protocol].append({ - 'value': applet.name, - 'label': applet.display_name, - 'icon': applet.icon, - }) - return methods - - class TerminalType(TextChoices): koko = 'koko', 'KoKo' guacamole = 'guacamole', 'Guacamole' @@ -182,107 +52,3 @@ class TerminalType(TextChoices): @classmethod def types(cls): return set(dict(cls.choices).keys()) - - @classmethod - def protocols(cls): - protocols = { - cls.koko: { - 'web_methods': [WebMethod.web_cli, WebMethod.web_sftp], - 'listen': [Protocol.ssh, Protocol.http], - 'support': [ - Protocol.ssh, Protocol.telnet, - Protocol.mysql, Protocol.postgresql, - Protocol.oracle, Protocol.sqlserver, - Protocol.mariadb, Protocol.clickhouse, - Protocol.redis, Protocol.mongodb, Protocol.k8s, - ], - 'match': 'm2m' - }, - cls.omnidb: { - 'web_methods': [WebMethod.web_gui], - 'listen': [Protocol.http], - 'support': [ - Protocol.mysql, Protocol.postgresql, Protocol.oracle, - Protocol.sqlserver, Protocol.mariadb - ], - 'match': 'm2m' - }, - cls.lion: { - 'web_methods': [WebMethod.web_gui], - 'listen': [Protocol.http], - 'support': [Protocol.rdp, Protocol.vnc], - 'match': 'm2m' - }, - cls.magnus: { - 'listen': [], - 'support': [ - Protocol.mysql, Protocol.postgresql, - Protocol.oracle, Protocol.mariadb - ], - 'match': 'map' - }, - cls.razor: { - 'listen': [Protocol.rdp], - 'support': [Protocol.rdp], - 'match': 'map' - }, - } - return protocols - - @classmethod - def get_connect_method(cls, name, protocol, os='linux'): - methods = cls.get_protocols_connect_methods(os) - protocol_methods = methods.get(protocol, []) - for method in protocol_methods: - if method['value'] == name: - return method - return None - - @classmethod - def get_protocols_connect_methods(cls, os): - methods = defaultdict(list) - web_methods = WebMethod.get_methods() - native_methods = NativeClient.get_methods(os) - applet_methods = AppletMethod.get_methods() - - for component, component_protocol in cls.protocols().items(): - support = component_protocol['support'] - - for protocol in support: - if component_protocol['match'] == 'map': - listen = [protocol] - else: - listen = component_protocol['listen'] - - for listen_protocol in listen: - # Native method - methods[protocol.value].extend([ - { - 'component': component.value, - 'type': 'native', - 'endpoint_protocol': listen_protocol, - **method - } - for method in native_methods[listen_protocol] - ]) - - protocol_web_methods = set(web_methods.get(protocol, [])) \ - & set(component_protocol.get('web_methods', [])) - methods[protocol.value].extend([ - { - 'component': component.value, - 'type': 'web', - 'endpoint_protocol': 'http', - 'value': method.value, - 'label': method.label, - } - for method in protocol_web_methods - ]) - - for protocol, applet_methods in applet_methods.items(): - for method in applet_methods: - method['type'] = 'applet' - method['listen'] = 'rdp' - method['component'] = cls.tinker.value - methods[protocol].extend(applet_methods) - return methods diff --git a/apps/terminal/management/__init__.py b/apps/terminal/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/terminal/management/commands/__init__.py b/apps/terminal/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/terminal/management/commands/install_builtin_applets.py b/apps/terminal/management/commands/install_builtin_applets.py new file mode 100644 index 000000000..d3909d16b --- /dev/null +++ b/apps/terminal/management/commands/install_builtin_applets.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = 'Install builtin applets' + + def handle(self, *args, **options): + from terminal.applets import install_or_update_builtin_applets + install_or_update_builtin_applets() diff --git a/apps/terminal/migrations/0016_commandstorage_replaystorage.py b/apps/terminal/migrations/0016_commandstorage_replaystorage.py index 1f1b04966..5eb17046a 100644 --- a/apps/terminal/migrations/0016_commandstorage_replaystorage.py +++ b/apps/terminal/migrations/0016_commandstorage_replaystorage.py @@ -24,7 +24,7 @@ class Migration(migrations.Migration): ('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('es', 'Elasticsearch')], default='server', max_length=16, verbose_name='Type')), ('meta', common.db.fields.EncryptJsonDictTextField(default={})), - ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ], options={ 'abstract': False, @@ -43,7 +43,7 @@ class Migration(migrations.Migration): ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type')), ('meta', common.db.fields.EncryptJsonDictTextField(default={})), - ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ], options={ 'abstract': False, diff --git a/apps/terminal/migrations/0062_auto_20221216_1529.py b/apps/terminal/migrations/0062_auto_20221216_1529.py new file mode 100644 index 000000000..295cc8b55 --- /dev/null +++ b/apps/terminal/migrations/0062_auto_20221216_1529.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-12-16 07:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0061_rename_system_user_command_account'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='comment', + field=models.TextField(blank=True, null=True, verbose_name='Comment'), + ), + migrations.AddField( + model_name='session', + name='type', + field=models.CharField(db_index=True, default='normal', max_length=16), + ), + ] diff --git a/apps/terminal/migrations/0063_applet_builtin.py b/apps/terminal/migrations/0063_applet_builtin.py new file mode 100644 index 000000000..1d991e180 --- /dev/null +++ b/apps/terminal/migrations/0063_applet_builtin.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-12-20 07:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0062_auto_20221216_1529'), + ] + + operations = [ + migrations.AddField( + model_name='applet', + name='builtin', + field=models.BooleanField(default=False, verbose_name='Builtin'), + ), + ] diff --git a/apps/terminal/migrations/0064_auto_20221220_1956.py b/apps/terminal/migrations/0064_auto_20221220_1956.py new file mode 100644 index 000000000..93b47dedc --- /dev/null +++ b/apps/terminal/migrations/0064_auto_20221220_1956.py @@ -0,0 +1,178 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0063_applet_builtin'), + ] + + operations = [ + migrations.AddField( + model_name='commandstorage', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='replaystorage', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='sessionjoinrecord', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='sessionjoinrecord', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='sessionreplay', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='sessionreplay', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='sessionsharing', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='sessionsharing', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='task', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='task', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='task', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='task', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='terminal', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='terminal', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='terminal', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='applet', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='applet', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='applethostdeployment', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='applethostdeployment', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='appletpublication', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='appletpublication', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='commandstorage', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='endpoint', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='endpoint', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='endpointrule', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='endpointrule', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='replaystorage', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='sessionjoinrecord', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='sessionreplay', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='sessionsharing', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='task', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='terminal', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='terminal', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + ] diff --git a/apps/terminal/migrations/0065_auto_20221223_1536.py b/apps/terminal/migrations/0065_auto_20221223_1536.py new file mode 100644 index 000000000..23b070be8 --- /dev/null +++ b/apps/terminal/migrations/0065_auto_20221223_1536.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.16 on 2022-12-23 07:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0064_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applet', + options={'verbose_name': 'Applet'}, + ), + migrations.AlterModelOptions( + name='applethost', + options={'verbose_name': 'Applet host'}, + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index cf854b036..b898cf2ed 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -1,14 +1,16 @@ -import yaml import os.path +import random +import yaml from django.conf import settings +from django.core.cache import cache from django.core.files.storage import default_storage from django.db import models from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import ValidationError from common.db.models import JMSBaseModel - __all__ = ['Applet', 'AppletPublication'] @@ -23,6 +25,7 @@ class Applet(JMSBaseModel): author = models.CharField(max_length=128, verbose_name=_('Author')) type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) + builtin = models.BooleanField(default=False, verbose_name=_('Builtin')) protocols = models.JSONField(default=list, verbose_name=_('Protocol')) tags = models.JSONField(default=list, verbose_name=_('Tags')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) @@ -31,12 +34,18 @@ class Applet(JMSBaseModel): to='AppletHost', verbose_name=_('Hosts') ) + class Meta: + verbose_name = _("Applet") + def __str__(self): return self.name @property def path(self): - return default_storage.path('applets/{}'.format(self.name)) + if self.builtin: + return os.path.join(settings.APPS_DIR, 'terminal', 'applets', self.name) + else: + return default_storage.path('applets/{}'.format(self.name)) @property def manifest(self): @@ -53,10 +62,71 @@ class Applet(JMSBaseModel): return None return os.path.join(settings.MEDIA_URL, 'applets', self.name, 'icon.png') + @staticmethod + def validate_pkg(d): + files = ['manifest.yml', 'icon.png', 'i18n.yml', 'setup.yml'] + for name in files: + path = os.path.join(d, name) + if not os.path.exists(path): + raise ValidationError({'error': 'Missing file {}'.format(path)}) + + with open(os.path.join(d, 'manifest.yml')) as f: + manifest = yaml.safe_load(f) + + if not manifest.get('name', ''): + raise ValidationError({'error': 'Missing name in manifest.yml'}) + return manifest + + @classmethod + def install_from_dir(cls, path): + from terminal.serializers import AppletSerializer + + manifest = cls.validate_pkg(path) + name = manifest['name'] + instance = cls.objects.filter(name=name).first() + serializer = AppletSerializer(instance=instance, data=manifest) + serializer.is_valid() + serializer.save(builtin=True) + return instance + + def select_host_account(self): + hosts = list(self.hosts.all()) + if not hosts: + return None + + host = random.choice(hosts) + using_keys = cache.keys('host_accounts_{}_*'.format(host.id)) or [] + accounts_used = cache.get_many(using_keys) + accounts = host.accounts.all().exclude(username__in=accounts_used) + + if not accounts: + accounts = host.accounts.all() + if not accounts: + return None + + account = random.choice(accounts) + ttl = 60 * 60 * 24 + lock_key = 'applet_host_accounts_{}_{}'.format(host.id, account.username) + cache.set(lock_key, account.username, ttl) + + return { + 'host': host, + 'account': account, + 'lock_key': lock_key, + 'ttl': ttl + } + + @staticmethod + def release_host_and_account(host_id, username): + key = 'applet_host_accounts_{}_{}'.format(host_id, username) + cache.delete(key) + class AppletPublication(JMSBaseModel): - applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet')) - host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host')) + applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', + verbose_name=_('Applet')) + host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', + verbose_name=_('Host')) status = models.CharField(max_length=16, default='ready', verbose_name=_('Status')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 020c4d98a..311fbe61a 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -29,6 +29,9 @@ class AppletHost(Host): ) LOCKING_ORG = '00000000-0000-0000-0000-000000000004' + class Meta: + verbose_name = _("Applet host") + def __str__(self): return self.name diff --git a/apps/terminal/models/component/storage.py b/apps/terminal/models/component/storage.py index b6878fb64..a3f8e4b54 100644 --- a/apps/terminal/models/component/storage.py +++ b/apps/terminal/models/component/storage.py @@ -1,22 +1,23 @@ from __future__ import unicode_literals + import copy import os from importlib import import_module import jms_storage +from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from common.mixins import CommonModelMixin +from common.db.fields import EncryptJsonDictTextField +from common.db.models import JMSBaseModel from common.plugins.es import QuerySet as ESQuerySet from common.utils import get_logger -from common.db.fields import EncryptJsonDictTextField from common.utils.timezone import local_now_date_display +from terminal import const from terminal.backends import TYPE_ENGINE_MAPPING from .terminal import Terminal from ..session.command import Command -from terminal import const logger = get_logger(__file__) @@ -25,7 +26,6 @@ class CommonStorageModelMixin(models.Model): name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True) meta = EncryptJsonDictTextField(default={}) is_default = models.BooleanField(default=False, verbose_name=_('Default storage')) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) class Meta: abstract = True @@ -51,7 +51,7 @@ class CommonStorageModelMixin(models.Model): return objs.first() -class CommandStorage(CommonStorageModelMixin, CommonModelMixin): +class CommandStorage(CommonStorageModelMixin, JMSBaseModel): type = models.CharField( max_length=16, choices=const.CommandStorageType.choices, default=const.CommandStorageType.server.value, verbose_name=_('Type'), @@ -140,7 +140,7 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin): verbose_name = _("Command storage") -class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): +class ReplayStorage(CommonStorageModelMixin, JMSBaseModel): type = models.CharField( max_length=16, choices=const.ReplayStorageType.choices, default=const.ReplayStorageType.server.value, verbose_name=_('Type') diff --git a/apps/terminal/models/component/task.py b/apps/terminal/models/component/task.py index 0225dc641..1c081da53 100644 --- a/apps/terminal/models/component/task.py +++ b/apps/terminal/models/component/task.py @@ -1,27 +1,24 @@ from __future__ import unicode_literals -import uuid - from django.db import models from django.utils.translation import ugettext_lazy as _ + +from common.db.models import JMSBaseModel from .terminal import Terminal -class Task(models.Model): +class Task(JMSBaseModel): NAME_CHOICES = ( ("kill_session", "Kill Session"), ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, choices=NAME_CHOICES, verbose_name=_("Name")) args = models.CharField(max_length=1024, verbose_name=_("Args")) kwargs = models.JSONField(default=dict, verbose_name=_("Kwargs")) terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) is_finished = models.BooleanField(default=False) - date_created = models.DateTimeField(auto_now_add=True) date_finished = models.DateTimeField(null=True) class Meta: db_table = "terminal_task" verbose_name = _("Task") - diff --git a/apps/terminal/models/component/terminal.py b/apps/terminal/models/component/terminal.py index e244c8a40..406d2192a 100644 --- a/apps/terminal/models/component/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -1,11 +1,11 @@ import time -import uuid from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from common.const.signals import SKIP_SIGNAL +from common.db.models import JMSBaseModel from common.utils import get_logger, lazyproperty from orgs.utils import tmp_to_root_org from terminal.const import TerminalType as TypeChoices @@ -75,8 +75,7 @@ class StorageMixin: return {"TERMINAL_REPLAY_STORAGE": config} -class Terminal(StorageMixin, TerminalStatusMixin, models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) type = models.CharField( choices=TypeChoices.choices, default=TypeChoices.koko, @@ -88,8 +87,6 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): user = models.OneToOneField(User, related_name='terminal', verbose_name=_('Application User'), null=True, on_delete=models.CASCADE) is_deleted = models.BooleanField(default=False) - date_created = models.DateTimeField(auto_now_add=True) - comment = models.TextField(blank=True, verbose_name=_('Comment')) @property def is_active(self): diff --git a/apps/terminal/models/session/replay.py b/apps/terminal/models/session/replay.py index 5f3e372af..9fd210f48 100644 --- a/apps/terminal/models/session/replay.py +++ b/apps/terminal/models/session/replay.py @@ -1,11 +1,11 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.mixins.models import CommonModelMixin +from common.db.models import JMSBaseModel from .session import Session -class SessionReplay(CommonModelMixin): +class SessionReplay(JMSBaseModel): session = models.ForeignKey(Session, on_delete=models.CASCADE, verbose_name=_("Session")) class Meta: @@ -14,5 +14,3 @@ class SessionReplay(CommonModelMixin): ('upload_sessionreplay', _("Can upload session replay")), ('download_sessionreplay', _("Can download session replay")), ] - - diff --git a/apps/terminal/models/session/session.py b/apps/terminal/models/session/session.py index 0a095a401..0241f10e5 100644 --- a/apps/terminal/models/session/session.py +++ b/apps/terminal/models/session/session.py @@ -3,24 +3,23 @@ from __future__ import unicode_literals import os import uuid -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.utils import timezone from django.conf import settings -from django.core.files.storage import default_storage from django.core.cache import cache +from django.core.files.storage import default_storage +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ -from assets.models import Asset from assets.const import Protocol -from users.models import User -from orgs.mixins.models import OrgModelMixin -from django.db.models import TextChoices +from assets.models import Asset from common.utils import get_object_or_none, lazyproperty +from orgs.mixins.models import OrgModelMixin from terminal.backends import get_multi_command_storage +from users.models import User class Session(OrgModelMixin): - class LOGIN_FROM(TextChoices): + class LOGIN_FROM(models.TextChoices): ST = 'ST', 'SSH Terminal' RT = 'RT', 'RDP Terminal' WT = 'WT', 'Web Terminal' @@ -34,6 +33,7 @@ class Session(OrgModelMixin): account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True) protocol = models.CharField(default='ssh', max_length=16, db_index=True) login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) + type = models.CharField(max_length=16, default='normal', db_index=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) is_success = models.BooleanField(default=True, db_index=True) is_finished = models.BooleanField(default=False, db_index=True) @@ -42,6 +42,7 @@ class Session(OrgModelMixin): terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.DO_NOTHING, db_constraint=False) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) + comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) upload_to = 'replay' ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' diff --git a/apps/terminal/models/session/sharing.py b/apps/terminal/models/session/sharing.py index 8675ced01..b71cad900 100644 --- a/apps/terminal/models/session/sharing.py +++ b/apps/terminal/models/session/sharing.py @@ -1,20 +1,19 @@ import datetime from django.db import models -from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from common.db.models import JMSBaseModel +from orgs.mixins.models import OrgModelMixin from orgs.utils import tmp_to_root_org from users.models import User - -from common.mixins import CommonModelMixin -from orgs.mixins.models import OrgModelMixin from .session import Session - __all__ = ['SessionSharing', 'SessionJoinRecord'] -class SessionSharing(CommonModelMixin, OrgModelMixin): +class SessionSharing(JMSBaseModel, OrgModelMixin): session = models.ForeignKey( 'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session') ) @@ -33,7 +32,7 @@ class SessionSharing(CommonModelMixin, OrgModelMixin): users = models.TextField(blank=True, verbose_name=_("User")) class Meta: - ordering = ('-date_created', ) + ordering = ('-date_created',) verbose_name = _('Session sharing') permissions = [ ('add_supersessionsharing', _("Can add super session sharing")) @@ -71,7 +70,7 @@ class SessionSharing(CommonModelMixin, OrgModelMixin): return True, '' -class SessionJoinRecord(CommonModelMixin, OrgModelMixin): +class SessionJoinRecord(JMSBaseModel, OrgModelMixin): LOGIN_FROM = Session.LOGIN_FROM session = models.ForeignKey( @@ -112,7 +111,7 @@ class SessionJoinRecord(CommonModelMixin, OrgModelMixin): ) class Meta: - ordering = ('-date_joined', ) + ordering = ('-date_joined',) verbose_name = _("Session join record") def __str__(self): diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 148217306..184262ec4 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -1,9 +1,10 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.serializers import BulkOrgResourceModelSerializer - from assets.const import Protocol +from common.drf.fields import LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Session __all__ = [ @@ -12,21 +13,28 @@ __all__ = [ ] +class SessionType(models.TextChoices): + normal = 'normal', _('Normal') + tunnel = 'tunnel', _('Tunnel') + command = 'command', _('Command') + + class SessionSerializer(BulkOrgResourceModelSerializer): org_id = serializers.CharField(allow_blank=True) - terminal_display = serializers.CharField(read_only=True, label=_('Terminal display')) protocol = serializers.ChoiceField(choices=Protocol.choices, label=_("Protocol")) + type = LabeledChoiceField(choices=SessionType.choices, label=_("Type"), default=SessionType.normal) class Meta: model = Session fields_mini = ["id"] fields_small = fields_mini + [ - "user", "asset", "user_id", "asset_id", 'account', "protocol", - "login_from", "login_from_display", "remote_addr", "is_success", - "is_finished", "has_replay", "date_start", "date_end", + "user", "asset", "user_id", "asset_id", 'account', + "protocol", 'type', "login_from", "remote_addr", + "is_success", "is_finished", "has_replay", "has_command", + "date_start", "date_end", "comment" ] fields_fk = ["terminal", ] - fields_custom = ["can_replay", "can_join", "can_terminate", 'terminal_display'] + fields_custom = ["can_replay", "can_join", "can_terminate"] fields = fields_small + fields_fk + fields_custom extra_kwargs = { "protocol": {'label': _('Protocol')}, diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers.py index 7868159a8..046c1b122 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers.py @@ -4,17 +4,18 @@ from django.db.models.signals import post_save, post_delete from django.db.utils import ProgrammingError from django.dispatch import receiver +from django.utils.functional import LazyObject +from assets.models import Asset from common.signals import django_ready from common.utils import get_logger +from common.utils.connection import RedisPubSub from orgs.utils import tmp_to_builtin_org -from assets.models import Asset -from .utils import db_port_manager, DBPortManager from .models import Applet, AppletHost +from .utils import db_port_manager, DBPortManager db_port_manager: DBPortManager - logger = get_logger(__file__) @@ -27,6 +28,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs): with tmp_to_builtin_org(system=1): instance.generate_accounts() + applet_host_change_pub_sub.publish(True) + @receiver(post_save, sender=Applet) def on_applet_create(sender, instance, created=False, **kwargs): @@ -35,12 +38,14 @@ def on_applet_create(sender, instance, created=False, **kwargs): hosts = AppletHost.objects.all() instance.hosts.set(hosts) + applet_host_change_pub_sub.publish(True) + @receiver(django_ready) -def init_db_port_mapper(sender, **kwargs): +def check_db_port_mapper(sender, **kwargs): logger.info('Init db port mapper') try: - db_port_manager.init() + db_port_manager.check() except (ProgrammingError,) as e: pass @@ -59,3 +64,22 @@ def on_db_app_delete(sender, instance, **kwargs): if not instance.category != 'database': return db_port_manager.pop(instance) + + +class AppletHostPubSub(LazyObject): + def _setup(self): + self._wrapped = RedisPubSub('fm.applet_host_change') + + +@receiver(django_ready) +def subscribe_applet_host_change(sender, **kwargs): + logger.debug("Start subscribe for expire node assets id mapping from memory") + + def on_change(message): + from terminal.connect_methods import ConnectMethodUtil + ConnectMethodUtil.refresh_methods() + + applet_host_change_pub_sub.subscribe(on_change) + + +applet_host_change_pub_sub = AppletHostPubSub() diff --git a/apps/terminal/utils/db_port_mapper.py b/apps/terminal/utils/db_port_mapper.py index d343dd3cb..4ca8cebec 100644 --- a/apps/terminal/utils/db_port_mapper.py +++ b/apps/terminal/utils/db_port_mapper.py @@ -34,9 +34,22 @@ class DBPortManager(object): def magnus_listen_port_range(self): return settings.MAGNUS_PORTS - def init(self): + @staticmethod + def fetch_dbs(): with tmp_to_root_org(): - db_ids = Asset.objects.filter(platform__category=Category.DATABASE).values_list('id', flat=True) + dbs = Asset.objects.filter(platform__category=Category.DATABASE).order_by('id') + return dbs + + def check(self): + dbs = self.fetch_dbs() + for db in dbs: + port = self.get_port_by_db(db, raise_exception=False) + if not port: + self.add(db) + + def init(self): + dbs = self.fetch_dbs() + db_ids = dbs.values_list('id', flat=True) db_ids = [str(i) for i in db_ids] mapper = dict(zip(self.all_available_ports, list(db_ids))) self.set_mapper(mapper) diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 912d61bc3..a402e23a1 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -60,6 +60,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): def perform_create(self, serializer): instance = serializer.save() + instance.applicant = self.request.user + instance.save(update_fields=['applicant']) instance.open() @action(detail=False, methods=[POST], permission_classes=[RBACPermission, ]) diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py index 7b311799f..40fb314e3 100644 --- a/apps/tickets/handlers/base.py +++ b/apps/tickets/handlers/base.py @@ -1,3 +1,5 @@ +from html import escape + from django.utils.translation import ugettext as _ from django.template.loader import render_to_string @@ -96,11 +98,19 @@ class BaseHandler: approve_info = _('{} {} the ticket').format(user_display, state_display) context = self._diff_prev_approve_context(state) context.update({'approve_info': approve_info}) + body = self.reject_html_script( + render_to_string('tickets/ticket_approve_diff.html', context) + ) data = { - 'body': render_to_string('tickets/ticket_approve_diff.html', context), + 'body': body, 'user': user, 'user_display': str(user), 'type': 'state', 'state': state } return self.ticket.comments.create(**data) + + @staticmethod + def reject_html_script(unsafe_html): + safe_html = escape(unsafe_html) + return safe_html diff --git a/apps/tickets/migrations/0020_auto_20220817_1346.py b/apps/tickets/migrations/0020_auto_20220817_1346.py index 783021dc8..a79bf727a 100644 --- a/apps/tickets/migrations/0020_auto_20220817_1346.py +++ b/apps/tickets/migrations/0020_auto_20220817_1346.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.14 on 2022-08-17 05:46 import time + from django.db import migrations, models @@ -15,7 +16,7 @@ def migrate_system_to_account(apps, schema_editor): (apply_login_asset_ticket_model, 'apply_login_system_user', 'apply_login_account', False), ) - print("\n\tStart migrate system user to account") + print("\n Start migrate system user to account") for model, old_field, new_field, m2m in model_system_user_account: print("\t - migrate '{}'".format(model.__name__)) count = 0 @@ -40,12 +41,11 @@ def migrate_system_to_account(apps, schema_editor): updated.append(obj) model.objects.bulk_update(updated, [new_field]) print(" Migrate account: {}-{} using: {:.2f}s".format( - count - len(objects), count, time.time()-start + count - len(objects), count, time.time() - start )) class Migration(migrations.Migration): - dependencies = [ ('tickets', '0019_delete_applyapplicationticket'), ] diff --git a/apps/tickets/migrations/0025_auto_20221206_1820.py b/apps/tickets/migrations/0025_auto_20221206_1820.py index c82a17ce8..ca9947158 100644 --- a/apps/tickets/migrations/0025_auto_20221206_1820.py +++ b/apps/tickets/migrations/0025_auto_20221206_1820.py @@ -1,13 +1,12 @@ # Generated by Django 3.2.14 on 2022-12-06 10:20 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('acls', '0010_auto_20221205_1122'), + ('acls', '0008_commandgroup_comment'), ('tickets', '0024_auto_20221121_1800'), ] @@ -23,6 +22,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='applycommandticket', name='apply_from_cmd_filter_acl', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='acls.commandfilteracl', verbose_name='Command filter acl'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='acls.commandfilteracl', + verbose_name='Command filter acl'), ), ] diff --git a/apps/tickets/migrations/0026_auto_20221220_1956.py b/apps/tickets/migrations/0026_auto_20221220_1956.py new file mode 100644 index 000000000..67f590f7b --- /dev/null +++ b/apps/tickets/migrations/0026_auto_20221220_1956.py @@ -0,0 +1,98 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0025_auto_20221206_1820'), + ] + + operations = [ + migrations.AddField( + model_name='approvalrule', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='approvalrule', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='comment', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='comment', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='ticket', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='ticketassignee', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='ticketassignee', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='ticketflow', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='ticketflow', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='ticketstep', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AddField( + model_name='ticketstep', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='approvalrule', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='comment', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='ticket', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='ticketassignee', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='ticketflow', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='ticketstep', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/tickets/migrations/0027_alter_applycommandticket_apply_run_account.py b/apps/tickets/migrations/0027_alter_applycommandticket_apply_run_account.py new file mode 100644 index 000000000..b491cbbc0 --- /dev/null +++ b/apps/tickets/migrations/0027_alter_applycommandticket_apply_run_account.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-12-23 07:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0026_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterField( + model_name='applycommandticket', + name='apply_run_account', + field=models.CharField(default='', max_length=128, verbose_name='Account'), + ), + ] diff --git a/apps/tickets/models/comment.py b/apps/tickets/models/comment.py index f15057d7e..638ecdc4d 100644 --- a/apps/tickets/models/comment.py +++ b/apps/tickets/models/comment.py @@ -3,12 +3,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.mixins.models import CommonModelMixin +from common.db.models import JMSBaseModel __all__ = ['Comment'] -class Comment(CommonModelMixin): +class Comment(JMSBaseModel): class Type(models.TextChoices): state = 'state', _('State') common = 'common', _('common') @@ -28,7 +28,7 @@ class Comment(CommonModelMixin): state = models.CharField(max_length=16, null=True) class Meta: - ordering = ('date_created', ) + ordering = ('date_created',) verbose_name = _("Comment") def set_display_fields(self): diff --git a/apps/tickets/models/flow.py b/apps/tickets/models/flow.py index 4dd468f57..0b7518fc6 100644 --- a/apps/tickets/models/flow.py +++ b/apps/tickets/models/flow.py @@ -3,18 +3,17 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from users.models import User -from common.mixins.models import CommonModelMixin - +from common.db.models import JMSBaseModel from orgs.mixins.models import OrgModelMixin from orgs.models import Organization from orgs.utils import tmp_to_org, get_current_org_id +from users.models import User from ..const import TicketType, TicketLevel, TicketApprovalStrategy __all__ = ['TicketFlow', 'ApprovalRule'] -class ApprovalRule(CommonModelMixin): +class ApprovalRule(JMSBaseModel): level = models.SmallIntegerField( default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approve level') @@ -51,7 +50,7 @@ class ApprovalRule(CommonModelMixin): return assignees -class TicketFlow(CommonModelMixin, OrgModelMixin): +class TicketFlow(JMSBaseModel, OrgModelMixin): type = models.CharField( max_length=64, choices=TicketType.choices, default=TicketType.general, verbose_name=_("Type") diff --git a/apps/tickets/models/ticket/command_confirm.py b/apps/tickets/models/ticket/command_confirm.py index 632bb1ee9..f261bb147 100644 --- a/apps/tickets/models/ticket/command_confirm.py +++ b/apps/tickets/models/ticket/command_confirm.py @@ -10,7 +10,7 @@ class ApplyCommandTicket(Ticket): ) apply_run_asset = models.CharField(max_length=128, verbose_name=_('Run asset')) apply_run_command = models.CharField(max_length=4096, verbose_name=_('Run command')) - apply_run_account = models.CharField(max_length=128, default='', verbose_name=_('Run account')) + apply_run_account = models.CharField(max_length=128, default='', verbose_name=_('Account')) apply_from_session = models.ForeignKey( 'terminal.Session', on_delete=models.SET_NULL, null=True, verbose_name=_("Session") ) diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index 271b87166..863a1508f 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -5,17 +5,17 @@ from typing import Callable from django.db import models from django.db.models import Q -from django.forms import model_to_dict -from django.db.utils import IntegrityError from django.db.models.fields import related +from django.db.utils import IntegrityError +from django.forms import model_to_dict from django.utils.translation import ugettext_lazy as _ -from orgs.utils import tmp_to_org -from orgs.models import Organization +from common.db.encoder import ModelJSONFieldEncoder +from common.db.models import JMSBaseModel from common.exceptions import JMSException from common.utils.timezone import as_current_tz -from common.mixins.models import CommonModelMixin -from common.db.encoder import ModelJSONFieldEncoder +from orgs.models import Organization +from orgs.utils import tmp_to_org from tickets.const import ( TicketType, TicketStatus, TicketState, TicketLevel, StepState, StepStatus @@ -25,11 +25,12 @@ from tickets.handlers import get_ticket_handler from ..flow import TicketFlow __all__ = [ - 'Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager' + 'Ticket', 'TicketStep', 'TicketAssignee', + 'SuperTicket', 'SubTicketManager' ] -class TicketStep(CommonModelMixin): +class TicketStep(JMSBaseModel): ticket = models.ForeignKey( 'Ticket', related_name='ticket_steps', on_delete=models.CASCADE, verbose_name='Ticket' @@ -74,7 +75,7 @@ class TicketStep(CommonModelMixin): verbose_name = _("Ticket step") -class TicketAssignee(CommonModelMixin): +class TicketAssignee(JMSBaseModel): assignee = models.ForeignKey( 'users.User', related_name='ticket_assignees', on_delete=models.CASCADE, verbose_name='Assignee' @@ -267,7 +268,7 @@ class StatusMixin: return get_ticket_handler(ticket=self) -class Ticket(StatusMixin, CommonModelMixin): +class Ticket(StatusMixin, JMSBaseModel): title = models.CharField(max_length=256, verbose_name=_('Title')) type = models.CharField( max_length=64, choices=TicketType.choices, diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index a0938cd99..8bf7b8563 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -13,10 +13,18 @@ __all__ = ['ApplyAssetSerializer', 'ApproveAssetSerializer'] asset_or_node_help_text = _("Select at least one asset or node") +apply_help_text = _('Support fuzzy search, and display up to 10 items') + class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer): - apply_assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False, label=_('Apply assets')) - apply_nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False, label=_('Apply nodes')) + apply_assets = ObjectRelatedField( + queryset=Asset.objects, many=True, required=False, + label=_('Apply assets'), help_text=apply_help_text + ) + apply_nodes = ObjectRelatedField( + queryset=Node.objects, many=True, required=False, + label=_('Apply nodes'), help_text=apply_help_text + ) apply_actions = ActionChoicesField(required=False, allow_null=True, label=_("Apply actions")) permission_model = AssetPermission diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index c445af4c8..d21c44ec0 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -61,7 +61,6 @@ class TicketApplySerializer(TicketSerializer): org_id = serializers.CharField( required=True, max_length=36, allow_blank=True, label=_("Organization") ) - applicant = serializers.CharField(required=False, allow_blank=True) def get_applicant(self, applicant_id): current_user = self.context['request'].user diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py index 01edf24b6..d5c584181 100644 --- a/apps/users/migrations/0001_initial.py +++ b/apps/users/migrations/0001_initial.py @@ -2,15 +2,16 @@ # Generated by Django 1.11 on 2017-12-21 16:06 from __future__ import unicode_literals -import common.utils -from django.contrib.auth.hashers import make_password -from django.conf import settings -import django.contrib.auth.models -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone import uuid +import django.contrib.auth.models +import django.utils.timezone +from django.conf import settings +from django.contrib.auth.hashers import make_password +from django.db import migrations, models + +import common.utils + def add_default_group(apps, schema_editor): group_model = apps.get_model("users", "UserGroup") @@ -34,7 +35,6 @@ def add_default_admin(apps, schema_editor): class Migration(migrations.Migration): - initial = True dependencies = [ @@ -49,13 +49,17 @@ class Migration(migrations.Migration): ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('is_active', models.BooleanField(default=True, + help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', + verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('username', models.CharField(max_length=20, unique=True, verbose_name='Username')), ('name', models.CharField(max_length=20, verbose_name='Name')), ('email', models.EmailField(max_length=30, unique=True, verbose_name='Email')), - ('role', models.CharField(blank=True, choices=[('Admin', 'Administrator'), ('User', 'User'), ('App', 'Application')], default='User', max_length=10, verbose_name='Role')), + ('role', models.CharField(blank=True, choices=[('Admin', 'Administrator'), ('User', 'User'), + ('App', 'Application')], default='User', max_length=10, + verbose_name='Role')), ('avatar', models.ImageField(null=True, upload_to='avatar', verbose_name='Avatar')), ('wechat', models.CharField(blank=True, max_length=30, verbose_name='Wechat')), ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), @@ -63,9 +67,10 @@ class Migration(migrations.Migration): ('secret_key_otp', models.CharField(blank=True, max_length=16)), ('_private_key', models.CharField(blank=True, max_length=5000, verbose_name='Private key')), ('_public_key', models.CharField(blank=True, max_length=5000, verbose_name='Public key')), - ('comment', models.TextField(blank=True, max_length=200, verbose_name='Comment')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), ('is_first_login', models.BooleanField(default=False)), - ('date_expired', models.DateTimeField(blank=True, default=common.utils.date_expired_default, null=True, verbose_name='Date expired')), + ('date_expired', models.DateTimeField(blank=True, default=common.utils.date_expired_default, null=True, + verbose_name='Date expired')), ('created_by', models.CharField(default='', max_length=30, verbose_name='Created by')), ], options={ @@ -78,9 +83,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AccessKey', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='AccessKeyID')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, + verbose_name='AccessKeyID')), ('secret', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='AccessKeySecret')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_key', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_key', + to=settings.AUTH_USER_MODEL, verbose_name='User')), ], ), migrations.CreateModel( @@ -88,7 +95,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('username', models.CharField(max_length=20, verbose_name='Username')), - ('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')], max_length=2, verbose_name='Login type')), + ('type', + models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')], max_length=2, verbose_name='Login type')), ('ip', models.GenericIPAddressField(verbose_name='Login ip')), ('city', models.CharField(blank=True, max_length=254, null=True, verbose_name='Login city')), ('user_agent', models.CharField(blank=True, max_length=254, null=True, verbose_name='User agent')), @@ -103,7 +111,8 @@ class Migration(migrations.Migration): fields=[ ('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token', + to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ 'verbose_name': 'Private Token', @@ -127,12 +136,15 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='groups', - field=models.ManyToManyField(blank=True, related_name='users', to='users.UserGroup', verbose_name='User group'), + field=models.ManyToManyField(blank=True, related_name='users', to='users.UserGroup', + verbose_name='User group'), ), migrations.AddField( model_name='user', name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', + related_name='user_set', related_query_name='user', to='auth.Permission', + verbose_name='user permissions'), ), migrations.RunPython(add_default_group), migrations.RunPython(add_default_admin), diff --git a/apps/users/migrations/0041_auto_20221220_1956.py b/apps/users/migrations/0041_auto_20221220_1956.py new file mode 100644 index 000000000..9ce1be2fe --- /dev/null +++ b/apps/users/migrations/0041_auto_20221220_1956.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.14 on 2022-12-20 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0040_alter_user_source'), + ] + + operations = [ + migrations.AddField( + model_name='usergroup', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='usergroup', + name='updated_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='usergroup', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='usergroup', + name='created_by', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/users/models/group.py b/apps/users/models/group.py index d06fce989..e92e65c81 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -1,22 +1,16 @@ # -*- coding: utf-8 -*- -import uuid -from django.db import models, IntegrityError +from django.db import models from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty -from orgs.mixins.models import OrgModelMixin +from orgs.mixins.models import JMSOrgBaseModel __all__ = ['UserGroup'] -class UserGroup(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +class UserGroup(JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now_add=True, null=True, - verbose_name=_('Date created')) - created_by = models.CharField(max_length=100, null=True, blank=True) def __str__(self): return self.name @@ -27,7 +21,7 @@ class UserGroup(OrgModelMixin): class Meta: ordering = ['name'] - unique_together = [('org_id', 'name'),] + unique_together = [('org_id', 'name'), ] verbose_name = _("User group") @classmethod diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 2534837b5..b77d1151f 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -742,13 +742,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def __str__(self): return '{0.name}({0.username})'.format(self) - @classmethod - def get_group_ids_by_user_id(cls, user_id): - group_ids = cls.groups.through.objects.filter(user_id=user_id) \ - .distinct().values_list('usergroup_id', flat=True) - group_ids = list(group_ids) - return group_ids - @property def receive_backends(self): return self.user_msg_subscription.receive_backends @@ -871,7 +864,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def delete(self, using=None, keep_parents=False): if self.pk == 1 or self.username == 'admin': return - return super(User, self).delete() + return super(User, self).delete(using=using, keep_parents=keep_parents) @classmethod def get_user_allowed_auth_backend_paths(cls, username): diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index d84a7baa0..1d99be5ac 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- # from functools import partial + from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from common.drf.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField from common.drf.serializers import CommonBulkSerializerMixin -from common.validators import PhoneValidator from common.utils import pretty_string, get_logger -from common.drf.fields import EncryptedField +from common.validators import PhoneValidator from rbac.builtin import BuiltinRole -from rbac.permissions import RBACPermission from rbac.models import OrgRoleBinding, SystemRoleBinding, Role -from ..models import User +from rbac.permissions import RBACPermission from ..const import PasswordStrategy +from ..models import User __all__ = [ "UserSerializer", @@ -25,19 +26,14 @@ logger = get_logger(__file__) class RolesSerializerMixin(serializers.Serializer): - system_roles = serializers.ManyRelatedField( - child_relation=serializers.PrimaryKeyRelatedField(queryset=Role.system_roles), - label=_("System roles"), + system_roles = ObjectRelatedField( + queryset=Role.system_roles, attrs=('id', 'display_name'), + label=_("System roles"), many=True ) - org_roles = serializers.ManyRelatedField( - required=False, - child_relation=serializers.PrimaryKeyRelatedField(queryset=Role.org_roles), - label=_("Org roles"), + org_roles = ObjectRelatedField( + queryset=Role.org_roles, attrs=('id', 'display_name'), + label=_("Org roles"), many=True ) - system_roles_display = serializers.SerializerMethodField( - label=_("System roles display") - ) - org_roles_display = serializers.SerializerMethodField(label=_("Org roles display")) @staticmethod def get_system_roles_display(user): @@ -60,8 +56,8 @@ class RolesSerializerMixin(serializers.Serializer): if action in ("partial_bulk_update", "bulk_update", "partial_update", "update"): action = "create" model_cls_field_mapper = { - SystemRoleBinding: ["system_roles", "system_roles_display"], - OrgRoleBinding: ["org_roles", "system_roles_display"], + SystemRoleBinding: ["system_roles"], + OrgRoleBinding: ["org_roles"], } for model_cls, fields_names in model_cls_field_mapper.items(): @@ -79,10 +75,8 @@ class RolesSerializerMixin(serializers.Serializer): return fields -class UserSerializer( - RolesSerializerMixin, CommonBulkSerializerMixin, serializers.ModelSerializer -): - password_strategy = serializers.ChoiceField( +class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializers.ModelSerializer): + password_strategy = LabeledChoiceField( choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False, @@ -93,13 +87,11 @@ class UserSerializer( mfa_force_enabled = serializers.BooleanField( read_only=True, label=_("MFA force enabled") ) - mfa_level_display = serializers.ReadOnlyField( - source="get_mfa_level_display", label=_("MFA level display") - ) login_blocked = serializers.BooleanField(read_only=True, label=_("Login blocked")) is_expired = serializers.BooleanField(read_only=True, label=_("Is expired")) - can_public_key_auth = serializers.ReadOnlyField( - source="can_use_ssh_key_login", label=_("Can public key authentication") + can_public_key_auth = serializers.BooleanField( + source="can_use_ssh_key_login", label=_("Can public key authentication"), + read_only=True ) password = EncryptedField( label=_("Password"), @@ -108,9 +100,6 @@ class UserSerializer( allow_null=True, max_length=1024, ) - # Todo: 这里看看该怎么搞 - # can_update = serializers.SerializerMethodField(label=_('Can update')) - # can_delete = serializers.SerializerMethodField(label=_('Can delete')) custom_m2m_fields = { "system_roles": [BuiltinRole.system_user], "org_roles": [BuiltinRole.org_user], @@ -122,44 +111,22 @@ class UserSerializer( fields_mini = ["id", "name", "username"] # 只能写的字段, 这个虽然无法在框架上生效,但是更多对我们是提醒 fields_write_only = [ - "password", - "public_key", + "password", "public_key", ] # small 指的是 不需要计算的直接能从一张表中获取到的数据 - fields_small = ( - fields_mini - + fields_write_only - + [ - "email", - "wechat", - "phone", - "mfa_level", - "source", - "source_display", - "can_public_key_auth", - "need_update_password", - "mfa_enabled", - "is_service_account", - "is_valid", - "is_expired", - "is_active", # 布尔字段 - "date_expired", - "date_joined", - "last_login", # 日期字段 - "created_by", - "comment", # 通用字段 - "is_wecom_bound", - "is_dingtalk_bound", - "is_feishu_bound", - "is_otp_secret_key_bound", - "wecom_id", - "dingtalk_id", - "feishu_id", - ] - ) + fields_small = fields_mini + fields_write_only + [ + "email", "wechat", "phone", "mfa_level", "source", + "need_update_password", "mfa_enabled", + "is_service_account", "is_valid", + "is_expired", "is_active", # 布尔字段 + "is_otp_secret_key_bound", "can_public_key_auth", + "date_expired", "date_joined", + "last_login", # 日期字段 + "created_by", "comment", # 通用字段 + "wecom_id", "dingtalk_id", "feishu_id", + ] # 包含不太常用的字段,可以没有 fields_verbose = fields_small + [ - "mfa_level_display", "mfa_force_enabled", "is_first_login", "date_password_last_updated", @@ -168,25 +135,14 @@ class UserSerializer( # 外键的字段 fields_fk = [] # 多对多字段 - fields_m2m = [ - "groups", - "groups_display", - "system_roles", - "org_roles", - "system_roles_display", - "org_roles_display", - ] + fields_m2m = ["groups", "system_roles", "org_roles", ] # 在serializer 上定义的字段 fields_custom = ["login_blocked", "password_strategy"] fields = fields_verbose + fields_fk + fields_m2m + fields_custom read_only_fields = [ - "date_joined", - "last_login", - "created_by", - "is_first_login", - "wecom_id", - "dingtalk_id", + "date_joined", "last_login", "created_by", + "is_first_login", "wecom_id", "dingtalk_id", "feishu_id", ] disallow_self_update_fields = ["is_active"] @@ -205,18 +161,9 @@ class UserSerializer( "is_expired": {"label": _("Is expired")}, "avatar_url": {"label": _("Avatar url")}, "created_by": {"read_only": True, "allow_blank": True}, - "groups_display": {"label": _("Groups name")}, - "source_display": {"label": _("Source name")}, - "org_role_display": {"label": _("Organization role name")}, - "role_display": {"label": _("Super role name")}, - "total_role_display": {"label": _("Total role name")}, "role": {"default": "User"}, - "is_wecom_bound": {"label": _("Is wecom bound")}, - "is_dingtalk_bound": {"label": _("Is dingtalk bound")}, - "is_feishu_bound": {"label": _("Is feishu bound")}, "is_otp_secret_key_bound": {"label": _("Is OTP bound")}, "phone": {"validators": [PhoneValidator()]}, - "system_role_display": {"label": _("System role name")}, } def validate_password(self, password): @@ -326,8 +273,6 @@ class InviteSerializer(RolesSerializerMixin, serializers.Serializer): help_text=_("For security, only list several users"), ) system_roles = None - system_roles_display = None - org_roles_display = None class ServiceAccountSerializer(serializers.ModelSerializer): diff --git a/jms b/jms index 2bb8f64c4..91021767e 100755 --- a/jms +++ b/jms @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # coding: utf-8 -import os +import argparse import logging import logging.handlers -import time -import argparse +import os import sys +import time + import django -import requests from django.core import management from django.db.utils import OperationalError @@ -24,6 +24,7 @@ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datef try: from jumpserver import const + __version__ = const.VERSION except ImportError as e: print("Not found __version__: {}".format(e)) @@ -122,6 +123,14 @@ def download_ip_db(): download_file(src, path) +def install_builtin_applets(): + logging.info("Install builtin applets") + try: + management.call_command('install_builtin_applets', verbosity=0, interactive=False) + except: + pass + + def upgrade_db(): collect_static() perform_db_migrate() @@ -132,6 +141,7 @@ def prepare(): upgrade_db() expire_caches() download_ip_db() + install_builtin_applets() def start_services(): @@ -190,4 +200,3 @@ if __name__ == '__main__': collect_static() else: start_services() - diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 745a23286..f83762fd7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -25,7 +25,7 @@ paramiko==2.11.0 passlib==1.7.4 pyasn1==0.4.8 pycparser==2.21 -cryptography==37.0.4 +cryptography==38.0.4 pycryptodome==3.15.0 pycryptodomex==3.15.0 gmssl==3.2.1 @@ -39,7 +39,6 @@ jms-storage==0.0.44 simplejson==3.17.6 six==1.16.0 sshpubkeys==3.3.1 -sshtunnel==0.4.0 uritemplate==4.1.1 urllib3==1.26.9 vine==5.0.0 @@ -88,7 +87,6 @@ pytz==2022.1 # Runtime django-proxy==1.2.1 channels-redis==3.4.0 -channels==3.0.4 python-daemon==2.3.0 eventlet==0.33.1 greenlet==1.1.2 @@ -108,23 +106,6 @@ django-cas-ng==4.0.1 python-cas==1.5.0 django-auth-ldap==2.2.0 # Cloud req -qingcloud-sdk==1.2.12 -azure-mgmt-subscription==1.0.0 -azure-identity==1.5.0 -azure-mgmt-compute==4.6.2 -azure-mgmt-network==2.7.0 -google-cloud-compute==0.5.0 -alibabacloud_dysmsapi20170525==2.0.2 -python-novaclient==11.0.1 -python-keystoneclient==4.3.0 -bce-python-sdk==0.8.64 -tencentcloud-sdk-python==3.0.662 -aliyun-python-sdk-core-v3==2.9.1 -aliyun-python-sdk-ecs==4.10.1 -huaweicloud-sdk-python==1.0.21 -# python-keystoneclient need keystoneauth1>=3.4.0 -# huaweicloud-sdk-python need keystoneauth1<=3.4.0 -keystoneauth1==3.4.0 boto3==1.24.12 botocore==1.27.12 s3transfer==0.6.0 @@ -132,8 +113,6 @@ kubernetes==21.7.0 # DB requirements mysqlclient==2.1.0 PyMySQL==1.0.2 -oracledb==1.0.1 -psycopg2-binary==2.9.1 pymssql==2.2.5 django-mysql==3.9.0 django-redis==5.2.0 @@ -147,5 +126,3 @@ ipython==8.4.0 ForgeryPy3==0.3.1 django-debug-toolbar==3.5 Pympler==1.0.1 -IPy==1.1 -psycopg2==2.9.4 diff --git a/requirements/requirements_xpack.txt b/requirements/requirements_xpack.txt new file mode 100644 index 000000000..4f0f0ea7d --- /dev/null +++ b/requirements/requirements_xpack.txt @@ -0,0 +1,24 @@ +# Cloud req +qingcloud-sdk==1.2.12 +azure-mgmt-subscription==1.0.0 +azure-identity==1.5.0 +azure-mgmt-compute==4.6.2 +azure-mgmt-network==2.7.0 +google-cloud-compute==0.5.0 +alibabacloud_dysmsapi20170525==2.0.2 +python-novaclient==11.0.1 +python-keystoneclient==4.3.0 +bce-python-sdk==0.8.64 +tencentcloud-sdk-python==3.0.662 +aliyun-python-sdk-core-v3==2.9.1 +aliyun-python-sdk-ecs==4.10.1 +huaweicloud-sdk-python==1.0.21 +# python-keystoneclient need keystoneauth1>=3.4.0 +# huaweicloud-sdk-python need keystoneauth1<=3.4.0 +keystoneauth1==3.4.0 +# DB requirements +oracledb==1.0.1 +psycopg2-binary==2.9.1 +pymssql==2.2.5 +IPy==1.1 +psycopg2==2.9.4 diff --git a/utils/check_celery.sh b/utils/check_celery.sh index 4673e0918..ba2a8777a 100644 --- a/utils/check_celery.sh +++ b/utils/check_celery.sh @@ -1,7 +1,8 @@ #!/bin/bash -if [[ "$(ps axu | grep 'celery' | grep -v 'grep' | grep -cv 'defunct')" -gt "2" ]];then - exit 0 -else - exit 1 -fi \ No newline at end of file +set -e + +test -e /tmp/worker_ready_ansible +test -e /tmp/worker_ready_celery +test -e /tmp/worker_heartbeat_ansible && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_ansible))) -lt 10 +test -e /tmp/worker_heartbeat_celery && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_celery))) -lt 10 diff --git a/utils/generate_fake_data/resources/users.py b/utils/generate_fake_data/resources/users.py index 34a9d9703..c05e0793b 100644 --- a/utils/generate_fake_data/resources/users.py +++ b/utils/generate_fake_data/resources/users.py @@ -25,15 +25,6 @@ class UserGenerator(FakeDataGenerator): def pre_generate(self): self.group_ids = list(UserGroup.objects.all().values_list('id', flat=True)) - def set_org(self, users): - relations = [] - for u in users: - relations.append(OrganizationMember( - org_id=self.org.id, - user_id=u.id, - )) - OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True) - def set_groups(self, users): relations = [] for i in users: @@ -55,5 +46,4 @@ class UserGenerator(FakeDataGenerator): ) users.append(u) users = User.objects.bulk_create(users, ignore_conflicts=True) - self.set_org(users) self.set_groups(users) diff --git a/utils/upgrade.sh b/utils/upgrade.sh deleted file mode 100644 index f700c9f7d..000000000 --- a/utils/upgrade.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -if grep -q 'source /opt/autoenv/activate.sh' ~/.bashrc; then - echo -e "\033[31m 正在自动载入 python 环境 \033[0m" -else - echo -e "\033[31m 不支持自动升级,请参考 http://docs.jumpserver.org/zh/docs/upgrade.html 手动升级 \033[0m" - exit 0 -fi - -source ~/.bashrc - -cd `dirname $0`/ && cd .. && ./jms stop - -jumpserver_backup=/tmp/jumpserver_backup$(date -d "today" +"%Y%m%d_%H%M%S") -mkdir -p $jumpserver_backup -cp -r ./* $jumpserver_backup - -echo -e "\033[31m 是否需要备份Jumpserver数据库 \033[0m" -stty erase ^H -read -p "确认备份请按Y,否则按其他键跳过备份 " a -if [ "$a" == y -o "$a" == Y ];then - echo -e "\033[31m 正在备份数据库 \033[0m" - echo -e "\033[31m 请手动输入数据库信息 \033[0m" - read -p '请输入Jumpserver数据库ip:' DB_HOST - read -p '请输入Jumpserver数据库端口:' DB_PORT - read -p '请输入Jumpserver数据库名称:' DB_NAME - read -p '请输入有权限导出数据库的用户:' DB_USER - read -p '请输入该用户的密码:' DB_PASSWORD - mysqldump -h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PASSWORD $DB_NAME > /$jumpserver_backup/$DB_NAME$(date -d "today" +"%Y%m%d_%H%M%S").sql || { - echo -e "\033[31m 备份数据库失败,请检查输入是否有误 \033[0m" - exit 1 - } - echo -e "\033[31m 备份数据库完成 \033[0m" -else - echo -e "\033[31m 已取消备份数据库操作 \033[0m" -fi - -git pull && pip install -r requirements/requirements.txt && cd utils && sh make_migrations.sh - -cd .. && ./jms start all -d -echo -e "\033[31m 请检查jumpserver是否启动成功 \033[0m" -echo -e "\033[31m 备份文件存放于$jumpserver_backup目录 \033[0m" -stty erase ^? - -exit 0